feat(37-01): admin global item services, routes, and unit tests
- Add listGlobalItemsForAdmin: paginated with batched tag/ownerCount queries - Add updateGlobalItemById: partial update in transaction, syncs tags - Add deleteGlobalItem: nullifies FK refs, removes tag associations before delete - Create src/server/routes/admin-items.ts with GET/GET:id/PUT/DELETE endpoints - Mount adminItemRoutes at /items in admin.ts (protected by requireAuth+requireAdmin) - Extend global-item.service.test.ts with 13 new tests (all passing) Closes ADMN-02, ADMN-03, ADMN-04 (server side)
This commit is contained in:
89
src/server/routes/admin-items.ts
Normal file
89
src/server/routes/admin-items.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import { Hono } from "hono";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { parseId } from "../lib/params.ts";
|
||||||
|
import {
|
||||||
|
deleteGlobalItem,
|
||||||
|
getGlobalItemWithOwnerCount,
|
||||||
|
listGlobalItemsForAdmin,
|
||||||
|
updateGlobalItemById,
|
||||||
|
} from "../services/global-item.service.ts";
|
||||||
|
|
||||||
|
type Env = { Variables: { db?: any; userId?: number } };
|
||||||
|
|
||||||
|
const app = new Hono<Env>();
|
||||||
|
|
||||||
|
const updateGlobalItemAdminSchema = z.object({
|
||||||
|
manufacturerId: z.number().int().positive().optional(),
|
||||||
|
model: z.string().min(1).optional(),
|
||||||
|
category: z.string().nullable().optional(),
|
||||||
|
weightGrams: z.number().positive().nullable().optional(),
|
||||||
|
priceCents: z.number().int().nonnegative().nullable().optional(),
|
||||||
|
imageUrl: z.string().url().nullable().optional(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
|
sourceUrl: z.string().url().nullable().optional(),
|
||||||
|
imageCredit: z.string().nullable().optional(),
|
||||||
|
imageSourceUrl: z.string().url().nullable().optional(),
|
||||||
|
tags: z.array(z.string().min(1)).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/admin/items — paginated list with search + tag filter
|
||||||
|
app.get("/", async (c) => {
|
||||||
|
const db = c.get("db");
|
||||||
|
const q = c.req.query("q");
|
||||||
|
const tagsParam = c.req.query("tags");
|
||||||
|
const tagNames = tagsParam
|
||||||
|
? tagsParam
|
||||||
|
.split(",")
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
const offset = Number(c.req.query("offset") ?? "0");
|
||||||
|
const limit = Number(c.req.query("limit") ?? "50");
|
||||||
|
|
||||||
|
const result = await listGlobalItemsForAdmin(db, {
|
||||||
|
query: q || undefined,
|
||||||
|
tagNames,
|
||||||
|
offset: isNaN(offset) ? 0 : offset,
|
||||||
|
limit: isNaN(limit) || limit > 100 ? 50 : limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/admin/items/:id — single item with ownerCount
|
||||||
|
app.get("/:id", async (c) => {
|
||||||
|
const db = c.get("db");
|
||||||
|
const id = parseId(c.req.param("id"));
|
||||||
|
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
||||||
|
const item = await getGlobalItemWithOwnerCount(db, id);
|
||||||
|
if (!item) return c.json({ error: "Global item not found" }, 404);
|
||||||
|
return c.json(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/admin/items/:id — update item fields
|
||||||
|
app.put(
|
||||||
|
"/:id",
|
||||||
|
zValidator("json", updateGlobalItemAdminSchema),
|
||||||
|
async (c) => {
|
||||||
|
const db = c.get("db");
|
||||||
|
const id = parseId(c.req.param("id"));
|
||||||
|
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
||||||
|
const data = c.req.valid("json");
|
||||||
|
const item = await updateGlobalItemById(db, id, data);
|
||||||
|
if (!item) return c.json({ error: "Global item not found" }, 404);
|
||||||
|
return c.json(item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /api/admin/items/:id — delete item with FK cleanup
|
||||||
|
app.delete("/:id", async (c) => {
|
||||||
|
const db = c.get("db");
|
||||||
|
const id = parseId(c.req.param("id"));
|
||||||
|
if (!id) return c.json({ error: "Invalid item ID" }, 400);
|
||||||
|
const deleted = await deleteGlobalItem(db, id);
|
||||||
|
if (!deleted) return c.json({ error: "Global item not found" }, 404);
|
||||||
|
return c.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
export { app as adminItemRoutes };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { requireAdmin, requireAuth } from "../middleware/auth.ts";
|
import { requireAdmin, requireAuth } from "../middleware/auth.ts";
|
||||||
|
import { adminItemRoutes } from "./admin-items.ts";
|
||||||
|
|
||||||
type Env = { Variables: { db?: any; userId?: number } };
|
type Env = { Variables: { db?: any; userId?: number } };
|
||||||
|
|
||||||
@@ -13,4 +14,7 @@ app.get("/", async (c) => {
|
|||||||
return c.json({ ok: true });
|
return c.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin item management
|
||||||
|
app.route("/items", adminItemRoutes);
|
||||||
|
|
||||||
export { app as adminRoutes };
|
export { app as adminRoutes };
|
||||||
|
|||||||
@@ -88,6 +88,226 @@ export async function searchGlobalItems(
|
|||||||
return baseQuery.where(and(...conditions));
|
return baseQuery.where(and(...conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listGlobalItemsForAdmin(
|
||||||
|
db: Db,
|
||||||
|
opts: {
|
||||||
|
query?: string;
|
||||||
|
tagNames?: string[];
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const { query, tagNames, offset = 0, limit = 50 } = opts;
|
||||||
|
const conditions: SQL[] = [];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const escaped = query.replace(/%/g, "\\%").replace(/_/g, "\\_");
|
||||||
|
const pattern = `%${escaped}%`;
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
ilike(manufacturers.name, pattern),
|
||||||
|
ilike(globalItems.model, pattern),
|
||||||
|
)!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagNames && tagNames.length > 0) {
|
||||||
|
conditions.push(
|
||||||
|
sql`${globalItems.id} IN (
|
||||||
|
SELECT ${globalItemTags.globalItemId}
|
||||||
|
FROM ${globalItemTags}
|
||||||
|
JOIN ${tags} ON ${tags.id} = ${globalItemTags.tagId}
|
||||||
|
WHERE ${tags.name} IN (${sql.join(
|
||||||
|
tagNames.map((t) => sql`${t}`),
|
||||||
|
sql`, `,
|
||||||
|
)})
|
||||||
|
GROUP BY ${globalItemTags.globalItemId}
|
||||||
|
HAVING COUNT(DISTINCT ${tags.name}) = ${tagNames.length}
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
|
||||||
|
// 1. Total count
|
||||||
|
const [{ total }] = await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(globalItems)
|
||||||
|
.innerJoin(manufacturers, eq(globalItems.manufacturerId, manufacturers.id))
|
||||||
|
.where(whereClause);
|
||||||
|
|
||||||
|
// 2. Paginated items
|
||||||
|
const pageItems = await db
|
||||||
|
.select({
|
||||||
|
id: globalItems.id,
|
||||||
|
manufacturerId: globalItems.manufacturerId,
|
||||||
|
brand: manufacturers.name,
|
||||||
|
model: globalItems.model,
|
||||||
|
category: globalItems.category,
|
||||||
|
weightGrams: globalItems.weightGrams,
|
||||||
|
priceCents: globalItems.priceCents,
|
||||||
|
imageUrl: globalItems.imageUrl,
|
||||||
|
description: globalItems.description,
|
||||||
|
sourceUrl: globalItems.sourceUrl,
|
||||||
|
imageCredit: globalItems.imageCredit,
|
||||||
|
imageSourceUrl: globalItems.imageSourceUrl,
|
||||||
|
dominantColor: globalItems.dominantColor,
|
||||||
|
cropZoom: globalItems.cropZoom,
|
||||||
|
cropX: globalItems.cropX,
|
||||||
|
cropY: globalItems.cropY,
|
||||||
|
createdAt: globalItems.createdAt,
|
||||||
|
})
|
||||||
|
.from(globalItems)
|
||||||
|
.innerJoin(manufacturers, eq(globalItems.manufacturerId, manufacturers.id))
|
||||||
|
.where(whereClause)
|
||||||
|
.orderBy(manufacturers.name, globalItems.model)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
if (pageItems.length === 0) {
|
||||||
|
return { items: [], total: total ?? 0, hasMore: false, nextOffset: offset };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = pageItems.map((i) => i.id);
|
||||||
|
|
||||||
|
// 3. Batch fetch tags for this page
|
||||||
|
const tagRows = await db
|
||||||
|
.select({
|
||||||
|
globalItemId: globalItemTags.globalItemId,
|
||||||
|
name: tags.name,
|
||||||
|
})
|
||||||
|
.from(globalItemTags)
|
||||||
|
.innerJoin(tags, eq(tags.id, globalItemTags.tagId))
|
||||||
|
.where(sql`${globalItemTags.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`);
|
||||||
|
|
||||||
|
const tagsByItemId = new Map<number, string[]>();
|
||||||
|
for (const row of tagRows) {
|
||||||
|
const list = tagsByItemId.get(row.globalItemId) ?? [];
|
||||||
|
list.push(row.name);
|
||||||
|
tagsByItemId.set(row.globalItemId, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Batch fetch owner counts for this page
|
||||||
|
const ownerRows = await db
|
||||||
|
.select({
|
||||||
|
globalItemId: items.globalItemId,
|
||||||
|
ownerCount: count(),
|
||||||
|
})
|
||||||
|
.from(items)
|
||||||
|
.where(sql`${items.globalItemId} IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`)
|
||||||
|
.groupBy(items.globalItemId);
|
||||||
|
|
||||||
|
const ownerCountById = new Map<number, number>();
|
||||||
|
for (const row of ownerRows) {
|
||||||
|
if (row.globalItemId != null) {
|
||||||
|
ownerCountById.set(row.globalItemId, row.ownerCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enriched = pageItems.map((item) => ({
|
||||||
|
...item,
|
||||||
|
tags: tagsByItemId.get(item.id) ?? [],
|
||||||
|
ownerCount: ownerCountById.get(item.id) ?? 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nextOffset = offset + limit;
|
||||||
|
return {
|
||||||
|
items: enriched,
|
||||||
|
total: total ?? 0,
|
||||||
|
hasMore: nextOffset < (total ?? 0),
|
||||||
|
nextOffset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateGlobalItemById(
|
||||||
|
db: Db,
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
manufacturerId?: number;
|
||||||
|
model?: string;
|
||||||
|
category?: string | null;
|
||||||
|
weightGrams?: number | null;
|
||||||
|
priceCents?: number | null;
|
||||||
|
imageUrl?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
sourceUrl?: string | null;
|
||||||
|
imageCredit?: string | null;
|
||||||
|
imageSourceUrl?: string | null;
|
||||||
|
tags?: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
const { tags: tagNames, ...fields } = data;
|
||||||
|
|
||||||
|
// Build partial update — only set provided fields
|
||||||
|
const updateSet: Record<string, unknown> = {};
|
||||||
|
if (fields.manufacturerId !== undefined) updateSet.manufacturerId = fields.manufacturerId;
|
||||||
|
if (fields.model !== undefined) updateSet.model = fields.model;
|
||||||
|
if ("category" in fields) updateSet.category = fields.category ?? null;
|
||||||
|
if ("weightGrams" in fields) updateSet.weightGrams = fields.weightGrams ?? null;
|
||||||
|
if ("priceCents" in fields) updateSet.priceCents = fields.priceCents ?? null;
|
||||||
|
if ("imageUrl" in fields) updateSet.imageUrl = fields.imageUrl ?? null;
|
||||||
|
if ("description" in fields) updateSet.description = fields.description ?? null;
|
||||||
|
if ("sourceUrl" in fields) updateSet.sourceUrl = fields.sourceUrl ?? null;
|
||||||
|
if ("imageCredit" in fields) updateSet.imageCredit = fields.imageCredit ?? null;
|
||||||
|
if ("imageSourceUrl" in fields) updateSet.imageSourceUrl = fields.imageSourceUrl ?? null;
|
||||||
|
|
||||||
|
let item: typeof globalItems.$inferSelect | undefined;
|
||||||
|
if (Object.keys(updateSet).length > 0) {
|
||||||
|
const [updated] = await tx
|
||||||
|
.update(globalItems)
|
||||||
|
.set(updateSet)
|
||||||
|
.where(eq(globalItems.id, id))
|
||||||
|
.returning();
|
||||||
|
item = updated;
|
||||||
|
} else {
|
||||||
|
const [existing] = await tx
|
||||||
|
.select()
|
||||||
|
.from(globalItems)
|
||||||
|
.where(eq(globalItems.id, id));
|
||||||
|
item = existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
if (tagNames !== undefined) {
|
||||||
|
await syncGlobalItemTags(tx, id, tagNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGlobalItem(db: Db, id: number) {
|
||||||
|
return await db.transaction(async (tx) => {
|
||||||
|
// 1. Verify item exists
|
||||||
|
const [existing] = await tx
|
||||||
|
.select({ id: globalItems.id })
|
||||||
|
.from(globalItems)
|
||||||
|
.where(eq(globalItems.id, id));
|
||||||
|
|
||||||
|
if (!existing) return false;
|
||||||
|
|
||||||
|
// 2. Nullify user item links (FK: items.globalItemId → globalItems.id, no cascade)
|
||||||
|
await tx
|
||||||
|
.update(items)
|
||||||
|
.set({ globalItemId: null })
|
||||||
|
.where(eq(items.globalItemId, id));
|
||||||
|
|
||||||
|
// 3. Remove tag associations (FK: globalItemTags.globalItemId → globalItems.id, no cascade)
|
||||||
|
await tx
|
||||||
|
.delete(globalItemTags)
|
||||||
|
.where(eq(globalItemTags.globalItemId, id));
|
||||||
|
|
||||||
|
// 4. Delete the global item
|
||||||
|
await tx
|
||||||
|
.delete(globalItems)
|
||||||
|
.where(eq(globalItems.id, id));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getGlobalItemWithOwnerCount(db: Db = prodDb, id: number) {
|
export async function getGlobalItemWithOwnerCount(db: Db = prodDb, id: number) {
|
||||||
const [item] = await db
|
const [item] = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import {
|
|||||||
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
|
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
|
||||||
import {
|
import {
|
||||||
bulkUpsertGlobalItems,
|
bulkUpsertGlobalItems,
|
||||||
|
deleteGlobalItem,
|
||||||
getGlobalItemWithOwnerCount,
|
getGlobalItemWithOwnerCount,
|
||||||
|
listGlobalItemsForAdmin,
|
||||||
searchGlobalItems,
|
searchGlobalItems,
|
||||||
|
updateGlobalItemById,
|
||||||
upsertGlobalItem,
|
upsertGlobalItem,
|
||||||
} from "../../src/server/services/global-item.service.ts";
|
} from "../../src/server/services/global-item.service.ts";
|
||||||
import { createTestDb } from "../helpers/db.ts";
|
import { createTestDb } from "../helpers/db.ts";
|
||||||
@@ -503,3 +506,153 @@ describe("Global Item Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("listGlobalItemsForAdmin", () => {
|
||||||
|
let db: TestDb["db"];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ db } = await createTestDb());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty result when no items exist", async () => {
|
||||||
|
const result = await listGlobalItemsForAdmin(db);
|
||||||
|
expect(result.items).toHaveLength(0);
|
||||||
|
expect(result.total).toBe(0);
|
||||||
|
expect(result.hasMore).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns paginated items with total count", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Alpha" });
|
||||||
|
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Beta" });
|
||||||
|
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Gamma" });
|
||||||
|
|
||||||
|
const result = await listGlobalItemsForAdmin(db, { limit: 2, offset: 0 });
|
||||||
|
expect(result.items).toHaveLength(2);
|
||||||
|
expect(result.total).toBe(3);
|
||||||
|
expect(result.hasMore).toBe(true);
|
||||||
|
expect(result.nextOffset).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by query string (brand/model)", async () => {
|
||||||
|
const mfr = await insertManufacturer(db, "Salsa", "salsa");
|
||||||
|
await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Woodsmoke 700" });
|
||||||
|
const mfr2 = await insertManufacturer(db, "Apidura", "apidura");
|
||||||
|
await insertGlobalItem(db, { manufacturerId: mfr2.id, model: "Racing Saddle Bag" });
|
||||||
|
|
||||||
|
const result = await listGlobalItemsForAdmin(db, { query: "salsa" });
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0]!.model).toBe("Woodsmoke 700");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes tags and ownerCount per item", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Test Item" });
|
||||||
|
const tag = await insertTag(db, "bikepacking");
|
||||||
|
await tagGlobalItem(db, globalItem.id, tag.id!);
|
||||||
|
|
||||||
|
// Insert a user and item linking to the global item
|
||||||
|
const [user] = await db
|
||||||
|
.insert(schema.users)
|
||||||
|
.values({ logtoSub: "test-sub" })
|
||||||
|
.returning();
|
||||||
|
await insertItem(db, "My Test Item", user!.id, { globalItemId: globalItem.id });
|
||||||
|
|
||||||
|
const result = await listGlobalItemsForAdmin(db);
|
||||||
|
expect(result.items).toHaveLength(1);
|
||||||
|
expect(result.items[0]!.tags).toContain("bikepacking");
|
||||||
|
expect(result.items[0]!.ownerCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateGlobalItemById", () => {
|
||||||
|
let db: TestDb["db"];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ db } = await createTestDb());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for non-existent item", async () => {
|
||||||
|
const result = await updateGlobalItemById(db, 99999, { model: "Ghost" });
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates model field by id", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Original" });
|
||||||
|
|
||||||
|
await updateGlobalItemById(db, globalItem.id, { model: "Updated" });
|
||||||
|
|
||||||
|
const updated = await getGlobalItemWithOwnerCount(db, globalItem.id);
|
||||||
|
expect(updated?.model).toBe("Updated");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("syncs tags when tags array provided", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Item" });
|
||||||
|
|
||||||
|
await updateGlobalItemById(db, globalItem.id, { tags: ["cycling", "gravel"] });
|
||||||
|
|
||||||
|
const result = await listGlobalItemsForAdmin(db);
|
||||||
|
const found = result.items.find((i) => i.id === globalItem.id);
|
||||||
|
expect(found?.tags).toContain("cycling");
|
||||||
|
expect(found?.tags).toContain("gravel");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deleteGlobalItem", () => {
|
||||||
|
let db: TestDb["db"];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ db } = await createTestDb());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for non-existent item", async () => {
|
||||||
|
const result = await deleteGlobalItem(db, 99999);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes item and returns true", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "To Delete" });
|
||||||
|
|
||||||
|
const result = await deleteGlobalItem(db, globalItem.id);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
|
||||||
|
const found = await getGlobalItemWithOwnerCount(db, globalItem.id);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nullifies items.globalItemId before deleting", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Owned Item" });
|
||||||
|
const [user] = await db
|
||||||
|
.insert(schema.users)
|
||||||
|
.values({ logtoSub: "delete-test-sub" })
|
||||||
|
.returning();
|
||||||
|
const userItem = await insertItem(db, "User Item", user!.id, { globalItemId: globalItem.id });
|
||||||
|
|
||||||
|
await deleteGlobalItem(db, globalItem.id);
|
||||||
|
|
||||||
|
const [afterDelete] = await db
|
||||||
|
.select({ globalItemId: items.globalItemId })
|
||||||
|
.from(items)
|
||||||
|
.where(eq(items.id, userItem!.id));
|
||||||
|
expect(afterDelete?.globalItemId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes globalItemTags before deleting", async () => {
|
||||||
|
const mfr = await insertManufacturer(db);
|
||||||
|
const globalItem = await insertGlobalItem(db, { manufacturerId: mfr.id, model: "Tagged Delete" });
|
||||||
|
const tag = await insertTag(db, "delete-tag");
|
||||||
|
await tagGlobalItem(db, globalItem.id, tag.id!);
|
||||||
|
|
||||||
|
await deleteGlobalItem(db, globalItem.id);
|
||||||
|
|
||||||
|
const remainingTags = await db
|
||||||
|
.select()
|
||||||
|
.from(globalItemTags)
|
||||||
|
.where(eq(globalItemTags.globalItemId, globalItem.id));
|
||||||
|
expect(remainingTags).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user