feat(18-02): implement global item service, seed script, and seed integration
- searchGlobalItems with LIKE-based case-insensitive search and wildcard escaping - getGlobalItemWithOwnerCount with owner count from junction table - linkItemToGlobal/unlinkItemFromGlobal for item-global linking - seedGlobalItems idempotent seed from JSON catalog - Integrated seed into seedDefaults startup
This commit is contained in:
27
src/db/seed-global-items.ts
Normal file
27
src/db/seed-global-items.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { db as prodDb } from "./index.ts";
|
||||||
|
import { globalItems } from "./schema.ts";
|
||||||
|
import seedData from "./global-items-seed.json";
|
||||||
|
|
||||||
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seed the global items table with initial bikepacking gear data.
|
||||||
|
* Idempotent: skips if any rows already exist.
|
||||||
|
*/
|
||||||
|
export function seedGlobalItems(db: Db = prodDb) {
|
||||||
|
const existing = db.select().from(globalItems).limit(1).all();
|
||||||
|
if (existing.length > 0) return;
|
||||||
|
|
||||||
|
for (const item of seedData) {
|
||||||
|
db.insert(globalItems)
|
||||||
|
.values({
|
||||||
|
brand: item.brand,
|
||||||
|
model: item.model,
|
||||||
|
category: item.category ?? null,
|
||||||
|
weightGrams: item.weightGrams ?? null,
|
||||||
|
priceCents: item.priceCents ?? null,
|
||||||
|
description: item.description ?? null,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { db } from "./index.ts";
|
import { db } from "./index.ts";
|
||||||
import { categories } from "./schema.ts";
|
import { categories } from "./schema.ts";
|
||||||
|
import { seedGlobalItems } from "./seed-global-items.ts";
|
||||||
|
|
||||||
export function seedDefaults() {
|
export function seedDefaults() {
|
||||||
const existing = db.select().from(categories).all();
|
const existing = db.select().from(categories).all();
|
||||||
@@ -11,4 +12,7 @@ export function seedDefaults() {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed global items catalog
|
||||||
|
seedGlobalItems(db);
|
||||||
}
|
}
|
||||||
|
|||||||
79
src/server/services/global-item.service.ts
Normal file
79
src/server/services/global-item.service.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { count, eq, like, or, sql } from "drizzle-orm";
|
||||||
|
import { db as prodDb } from "../../db/index.ts";
|
||||||
|
import { globalItems, itemGlobalLinks } from "../../db/schema.ts";
|
||||||
|
|
||||||
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search global items by brand or model. SQLite LIKE is case-insensitive for ASCII.
|
||||||
|
* Escapes % and _ wildcard characters in user input.
|
||||||
|
*/
|
||||||
|
export function searchGlobalItems(db: Db = prodDb, query?: string) {
|
||||||
|
if (!query) {
|
||||||
|
return db.select().from(globalItems).all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape SQL LIKE wildcards
|
||||||
|
const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
||||||
|
const pattern = `%${escaped}%`;
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(globalItems)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
like(globalItems.brand, pattern),
|
||||||
|
like(globalItems.model, pattern),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single global item by ID with the count of user items linked to it.
|
||||||
|
*/
|
||||||
|
export function getGlobalItemWithOwnerCount(db: Db = prodDb, id: number) {
|
||||||
|
const item = db
|
||||||
|
.select()
|
||||||
|
.from(globalItems)
|
||||||
|
.where(eq(globalItems.id, id))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
const result = db
|
||||||
|
.select({ ownerCount: count() })
|
||||||
|
.from(itemGlobalLinks)
|
||||||
|
.where(eq(itemGlobalLinks.globalItemId, id))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return { ...item, ownerCount: result?.ownerCount ?? 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link a user's item to a global item. Throws on duplicate (unique constraint on itemId).
|
||||||
|
*/
|
||||||
|
export function linkItemToGlobal(
|
||||||
|
db: Db = prodDb,
|
||||||
|
itemId: number,
|
||||||
|
globalItemId: number,
|
||||||
|
) {
|
||||||
|
return db
|
||||||
|
.insert(itemGlobalLinks)
|
||||||
|
.values({ itemId, globalItemId })
|
||||||
|
.returning()
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the link between a user's item and any global item.
|
||||||
|
*/
|
||||||
|
export function unlinkItemFromGlobal(db: Db = prodDb, itemId: number) {
|
||||||
|
const result = db
|
||||||
|
.delete(itemGlobalLinks)
|
||||||
|
.where(eq(itemGlobalLinks.itemId, itemId))
|
||||||
|
.returning()
|
||||||
|
.all();
|
||||||
|
|
||||||
|
return result.length;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user