feat(19-02): add COALESCE merge for reference items in item service
- getAllItems and getItemById LEFT JOIN globalItems with COALESCE for name, weight, price, image - createItem accepts globalItemId and purchasePriceCents, stores brand+model as fallback name - duplicateItem preserves globalItemId and purchasePriceCents - updateItem type includes globalItemId and purchasePriceCents - 10 new tests for reference item creation and merged data retrieval
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { globalItems } from "../../src/db/schema.ts";
|
||||
import {
|
||||
createItem,
|
||||
deleteItem,
|
||||
@@ -168,6 +169,216 @@ describe("Item Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reference items (globalItemId)", () => {
|
||||
async function insertGlobalItem(
|
||||
testDb: any,
|
||||
data: {
|
||||
brand: string;
|
||||
model: string;
|
||||
weightGrams?: number;
|
||||
priceCents?: number;
|
||||
imageUrl?: string;
|
||||
},
|
||||
) {
|
||||
const [row] = await testDb
|
||||
.insert(globalItems)
|
||||
.values(data)
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
it("createItem with globalItemId creates a reference item with globalItemId set", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Big Agnes",
|
||||
model: "Copper Spur HV UL2",
|
||||
weightGrams: 1270,
|
||||
priceCents: 44995,
|
||||
});
|
||||
|
||||
const item = await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.globalItemId).toBe(gi.id);
|
||||
});
|
||||
|
||||
it("createItem with globalItemId stores brand+model as fallback name", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "MSR",
|
||||
model: "Hubba Hubba",
|
||||
weightGrams: 1540,
|
||||
});
|
||||
|
||||
const item = await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
expect(item?.name).toBe("MSR Hubba Hubba");
|
||||
});
|
||||
|
||||
it("getAllItems returns merged name from global item for reference items", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Nemo",
|
||||
model: "Tensor",
|
||||
weightGrams: 425,
|
||||
priceCents: 17995,
|
||||
});
|
||||
|
||||
await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all).toHaveLength(1);
|
||||
expect(all[0].name).toBe("Nemo Tensor");
|
||||
});
|
||||
|
||||
it("getAllItems returns merged weightGrams from global item for reference items", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Nemo",
|
||||
model: "Tensor",
|
||||
weightGrams: 425,
|
||||
priceCents: 17995,
|
||||
});
|
||||
|
||||
await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all[0].weightGrams).toBe(425);
|
||||
});
|
||||
|
||||
it("getAllItems returns merged priceCents from global item for reference items", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Nemo",
|
||||
model: "Tensor",
|
||||
weightGrams: 425,
|
||||
priceCents: 17995,
|
||||
});
|
||||
|
||||
await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all[0].priceCents).toBe(17995);
|
||||
});
|
||||
|
||||
it("getAllItems returns item's own imageFilename when set, ignoring global imageUrl", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Nemo",
|
||||
model: "Tensor",
|
||||
imageUrl: "https://example.com/global.jpg",
|
||||
});
|
||||
|
||||
await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
imageFilename: "local.jpg",
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all[0].imageFilename).toBe("local.jpg");
|
||||
});
|
||||
|
||||
it("getAllItems falls back to global imageUrl when item has no imageFilename", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Nemo",
|
||||
model: "Tensor",
|
||||
imageUrl: "https://example.com/global.jpg",
|
||||
});
|
||||
|
||||
await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all[0].imageFilename).toBe("https://example.com/global.jpg");
|
||||
});
|
||||
|
||||
it("getAllItems returns standalone item data unchanged", async () => {
|
||||
await createItem(db, userId, {
|
||||
name: "My Manual Tent",
|
||||
weightGrams: 1500,
|
||||
priceCents: 25000,
|
||||
categoryId: 1,
|
||||
});
|
||||
|
||||
const all = await getAllItems(db, userId);
|
||||
expect(all).toHaveLength(1);
|
||||
expect(all[0].name).toBe("My Manual Tent");
|
||||
expect(all[0].weightGrams).toBe(1500);
|
||||
expect(all[0].priceCents).toBe(25000);
|
||||
expect(all[0].globalItemId).toBeNull();
|
||||
});
|
||||
|
||||
it("getItemById returns merged data for a reference item", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "Thermarest",
|
||||
model: "NeoAir XLite",
|
||||
weightGrams: 340,
|
||||
priceCents: 20995,
|
||||
});
|
||||
|
||||
const created = await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const item = await getItemById(db, userId, created?.id);
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.name).toBe("Thermarest NeoAir XLite");
|
||||
expect(item?.weightGrams).toBe(340);
|
||||
expect(item?.priceCents).toBe(20995);
|
||||
expect(item?.globalItemId).toBe(gi.id);
|
||||
});
|
||||
|
||||
it("duplicateItem on a reference item preserves globalItemId", async () => {
|
||||
const gi = await insertGlobalItem(db, {
|
||||
brand: "MSR",
|
||||
model: "PocketRocket 2",
|
||||
weightGrams: 73,
|
||||
priceCents: 4995,
|
||||
});
|
||||
|
||||
const original = await createItem(db, userId, {
|
||||
name: "placeholder",
|
||||
categoryId: 1,
|
||||
globalItemId: gi.id,
|
||||
});
|
||||
|
||||
const copy = await duplicateItem(db, userId, original?.id);
|
||||
expect(copy).toBeDefined();
|
||||
expect(copy?.globalItemId).toBe(gi.id);
|
||||
});
|
||||
|
||||
it("createItem with purchasePriceCents stores the value", async () => {
|
||||
const item = await createItem(db, userId, {
|
||||
name: "Discounted Tent",
|
||||
categoryId: 1,
|
||||
purchasePriceCents: 29999,
|
||||
});
|
||||
|
||||
expect(item?.purchasePriceCents).toBe(29999);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cross-user isolation", () => {
|
||||
it("user cannot see other user's items", async () => {
|
||||
const userId2 = await createSecondTestUser(db);
|
||||
|
||||
Reference in New Issue
Block a user