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:
@@ -10,8 +10,11 @@ import {
|
||||
import { seedGlobalItems } from "../../src/db/seed-global-items.ts";
|
||||
import {
|
||||
bulkUpsertGlobalItems,
|
||||
deleteGlobalItem,
|
||||
getGlobalItemWithOwnerCount,
|
||||
listGlobalItemsForAdmin,
|
||||
searchGlobalItems,
|
||||
updateGlobalItemById,
|
||||
upsertGlobalItem,
|
||||
} from "../../src/server/services/global-item.service.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