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,6 +1,6 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import type { db as prodDb } from "../../db/index.ts";
|
||||
import { categories, items } from "../../db/schema.ts";
|
||||
import { categories, globalItems, items } from "../../db/schema.ts";
|
||||
import type { CreateItem } from "../../shared/types.ts";
|
||||
|
||||
type Db = typeof prodDb;
|
||||
@@ -9,15 +9,32 @@ export async function getAllItems(db: Db, userId: number) {
|
||||
return db
|
||||
.select({
|
||||
id: items.id,
|
||||
name: items.name,
|
||||
weightGrams: items.weightGrams,
|
||||
priceCents: items.priceCents,
|
||||
name: sql<string>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL
|
||||
THEN ${globalItems.brand} || ' ' || ${globalItems.model}
|
||||
ELSE ${items.name}
|
||||
END,
|
||||
${items.name}
|
||||
)`.as("name"),
|
||||
weightGrams: sql<number | null>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
|
||||
${items.weightGrams}
|
||||
)`.as("weight_grams"),
|
||||
priceCents: sql<number | null>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
|
||||
${items.priceCents}
|
||||
)`.as("price_cents"),
|
||||
purchasePriceCents: items.purchasePriceCents,
|
||||
quantity: items.quantity,
|
||||
categoryId: items.categoryId,
|
||||
notes: items.notes,
|
||||
productUrl: items.productUrl,
|
||||
imageFilename: items.imageFilename,
|
||||
imageFilename: sql<string | null>`COALESCE(
|
||||
${items.imageFilename},
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END
|
||||
)`.as("image_filename"),
|
||||
imageSourceUrl: items.imageSourceUrl,
|
||||
globalItemId: items.globalItemId,
|
||||
createdAt: items.createdAt,
|
||||
updatedAt: items.updatedAt,
|
||||
categoryName: categories.name,
|
||||
@@ -25,6 +42,7 @@ export async function getAllItems(db: Db, userId: number) {
|
||||
})
|
||||
.from(items)
|
||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||
.where(eq(items.userId, userId));
|
||||
}
|
||||
|
||||
@@ -32,18 +50,36 @@ export async function getItemById(db: Db, userId: number, id: number) {
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: items.id,
|
||||
name: items.name,
|
||||
weightGrams: items.weightGrams,
|
||||
priceCents: items.priceCents,
|
||||
name: sql<string>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL
|
||||
THEN ${globalItems.brand} || ' ' || ${globalItems.model}
|
||||
ELSE ${items.name}
|
||||
END,
|
||||
${items.name}
|
||||
)`.as("name"),
|
||||
weightGrams: sql<number | null>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
|
||||
${items.weightGrams}
|
||||
)`.as("weight_grams"),
|
||||
priceCents: sql<number | null>`COALESCE(
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
|
||||
${items.priceCents}
|
||||
)`.as("price_cents"),
|
||||
purchasePriceCents: items.purchasePriceCents,
|
||||
categoryId: items.categoryId,
|
||||
notes: items.notes,
|
||||
productUrl: items.productUrl,
|
||||
imageFilename: items.imageFilename,
|
||||
imageFilename: sql<string | null>`COALESCE(
|
||||
${items.imageFilename},
|
||||
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END
|
||||
)`.as("image_filename"),
|
||||
imageSourceUrl: items.imageSourceUrl,
|
||||
globalItemId: items.globalItemId,
|
||||
createdAt: items.createdAt,
|
||||
updatedAt: items.updatedAt,
|
||||
})
|
||||
.from(items)
|
||||
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||
.where(and(eq(items.id, id), eq(items.userId, userId)));
|
||||
|
||||
return row ?? null;
|
||||
@@ -58,10 +94,22 @@ export async function createItem(
|
||||
imageFilename?: string;
|
||||
},
|
||||
) {
|
||||
// For reference items, look up global item for fallback name (items.name is NOT NULL)
|
||||
let name = data.name;
|
||||
if (data.globalItemId) {
|
||||
const [gi] = await db
|
||||
.select({ brand: globalItems.brand, model: globalItems.model })
|
||||
.from(globalItems)
|
||||
.where(eq(globalItems.id, data.globalItemId));
|
||||
if (gi) {
|
||||
name = `${gi.brand} ${gi.model}`;
|
||||
}
|
||||
}
|
||||
|
||||
const [row] = await db
|
||||
.insert(items)
|
||||
.values({
|
||||
name: data.name,
|
||||
name,
|
||||
weightGrams: data.weightGrams ?? null,
|
||||
priceCents: data.priceCents ?? null,
|
||||
quantity: data.quantity ?? 1,
|
||||
@@ -71,6 +119,8 @@ export async function createItem(
|
||||
productUrl: data.productUrl ?? null,
|
||||
imageFilename: data.imageFilename ?? null,
|
||||
imageSourceUrl: data.imageSourceUrl ?? null,
|
||||
globalItemId: data.globalItemId ?? null,
|
||||
purchasePriceCents: data.purchasePriceCents ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -91,6 +141,8 @@ export async function updateItem(
|
||||
productUrl: string;
|
||||
imageFilename: string;
|
||||
imageSourceUrl: string;
|
||||
globalItemId: number;
|
||||
purchasePriceCents: number;
|
||||
}>,
|
||||
) {
|
||||
// Check if item exists and belongs to user
|
||||
@@ -131,6 +183,8 @@ export async function duplicateItem(db: Db, userId: number, id: number) {
|
||||
imageFilename: source.imageFilename,
|
||||
imageSourceUrl: source.imageSourceUrl,
|
||||
quantity: source.quantity,
|
||||
globalItemId: source.globalItemId,
|
||||
purchasePriceCents: source.purchasePriceCents,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user