From 0a233c754dff1ccc475aae9f762fed16166f27b1 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 6 Apr 2026 00:26:13 +0200 Subject: [PATCH] feat(19-03): add COALESCE merge for reference items in secondary services - Setup service: LEFT JOIN globalItems in getAllSetups totals and getSetupWithItems - Totals service: LEFT JOIN globalItems in getCategoryTotals and getGlobalTotals - Profile service: LEFT JOIN globalItems in getPublicProfile totals and getPublicSetupWithItems - CSV service: LEFT JOIN globalItems in exportItemsCsv for merged name/weight/price --- src/server/services/csv.service.ts | 23 +++++++++--- src/server/services/profile.service.ts | 42 ++++++++++++++++++---- src/server/services/setup.service.ts | 50 ++++++++++++++++++++++---- src/server/services/totals.service.ts | 32 ++++++++++++++--- 4 files changed, 124 insertions(+), 23 deletions(-) diff --git a/src/server/services/csv.service.ts b/src/server/services/csv.service.ts index 7bdc520..d57cb08 100644 --- a/src/server/services/csv.service.ts +++ b/src/server/services/csv.service.ts @@ -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 { getOrCreateUncategorized } from "./category.service.ts"; type Db = typeof prodDb; @@ -88,16 +88,29 @@ function parseCsv(content: string): { headers: string[]; rows: string[][] } { export async function exportItemsCsv(db: Db, userId: number): Promise { const rows = await db .select({ - name: items.name, + name: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL + THEN ${globalItems.brand} || ' ' || ${globalItems.model} + ELSE ${items.name} + END, + ${items.name} + )`.as("name"), quantity: items.quantity, - weightGrams: items.weightGrams, - priceCents: items.priceCents, + weightGrams: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END, + ${items.weightGrams} + )`.as("weight_grams"), + priceCents: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END, + ${items.priceCents} + )`.as("price_cents"), categoryName: categories.name, notes: items.notes, productUrl: items.productUrl, }) .from(items) .innerJoin(categories, eq(items.categoryId, categories.id)) + .leftJoin(globalItems, eq(items.globalItemId, globalItems.id)) .where(eq(items.userId, userId)); const header = diff --git a/src/server/services/profile.service.ts b/src/server/services/profile.service.ts index 2afb514..5c0f31e 100644 --- a/src/server/services/profile.service.ts +++ b/src/server/services/profile.service.ts @@ -2,6 +2,7 @@ import { and, eq, sql } from "drizzle-orm"; import type { db as prodDb } from "../../db/index.ts"; import { categories, + globalItems, items, setupItems, setups, @@ -55,13 +56,25 @@ export async function getPublicProfile(db: Db, userId: number) { WHERE setup_items.setup_id = setups.id ), 0)`.as("item_count"), totalWeight: sql`COALESCE(( - SELECT SUM(items.weight_grams * items.quantity) FROM setup_items + SELECT SUM( + COALESCE( + CASE WHEN items.global_item_id IS NOT NULL THEN global_items.weight_grams ELSE NULL END, + items.weight_grams + ) * items.quantity + ) FROM setup_items JOIN items ON items.id = setup_items.item_id + LEFT JOIN global_items ON global_items.id = items.global_item_id WHERE setup_items.setup_id = setups.id ), 0)`.as("total_weight"), totalCost: sql`COALESCE(( - SELECT SUM(items.price_cents * items.quantity) FROM setup_items + SELECT SUM( + COALESCE( + CASE WHEN items.global_item_id IS NOT NULL THEN global_items.price_cents ELSE NULL END, + items.price_cents + ) * items.quantity + ) FROM setup_items JOIN items ON items.id = setup_items.item_id + LEFT JOIN global_items ON global_items.id = items.global_item_id WHERE setup_items.setup_id = setups.id ), 0)`.as("total_cost"), }) @@ -82,14 +95,30 @@ export async function getPublicSetupWithItems(db: Db, setupId: number) { const itemList = await db .select({ id: items.id, - name: items.name, - weightGrams: items.weightGrams, - priceCents: items.priceCents, + name: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL + THEN ${globalItems.brand} || ' ' || ${globalItems.model} + ELSE ${items.name} + END, + ${items.name} + )`.as("name"), + weightGrams: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END, + ${items.weightGrams} + )`.as("weight_grams"), + priceCents: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END, + ${items.priceCents} + )`.as("price_cents"), quantity: items.quantity, categoryId: items.categoryId, notes: items.notes, productUrl: items.productUrl, - imageFilename: items.imageFilename, + imageFilename: sql`COALESCE( + ${items.imageFilename}, + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END + )`.as("image_filename"), + globalItemId: items.globalItemId, createdAt: items.createdAt, updatedAt: items.updatedAt, categoryName: categories.name, @@ -99,6 +128,7 @@ export async function getPublicSetupWithItems(db: Db, setupId: number) { .from(setupItems) .innerJoin(items, eq(setupItems.itemId, items.id)) .innerJoin(categories, eq(items.categoryId, categories.id)) + .leftJoin(globalItems, eq(items.globalItemId, globalItems.id)) .where(eq(setupItems.setupId, setupId)); return { ...setup, items: itemList }; diff --git a/src/server/services/setup.service.ts b/src/server/services/setup.service.ts index 95f502a..8798377 100644 --- a/src/server/services/setup.service.ts +++ b/src/server/services/setup.service.ts @@ -1,6 +1,12 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import type { db as prodDb } from "../../db/index.ts"; -import { categories, items, setupItems, setups } from "../../db/schema.ts"; +import { + categories, + globalItems, + items, + setupItems, + setups, +} from "../../db/schema.ts"; import type { CreateSetup, UpdateSetup } from "../../shared/types.ts"; type Db = typeof prodDb; @@ -27,13 +33,25 @@ export async function getAllSetups(db: Db, userId: number) { WHERE setup_items.setup_id = setups.id ), 0)`.as("item_count"), totalWeight: sql`COALESCE(( - SELECT SUM(items.weight_grams * items.quantity) FROM setup_items + SELECT SUM( + COALESCE( + CASE WHEN items.global_item_id IS NOT NULL THEN global_items.weight_grams ELSE NULL END, + items.weight_grams + ) * items.quantity + ) FROM setup_items JOIN items ON items.id = setup_items.item_id + LEFT JOIN global_items ON global_items.id = items.global_item_id WHERE setup_items.setup_id = setups.id ), 0)`.as("total_weight"), totalCost: sql`COALESCE(( - SELECT SUM(items.price_cents * items.quantity) FROM setup_items + SELECT SUM( + COALESCE( + CASE WHEN items.global_item_id IS NOT NULL THEN global_items.price_cents ELSE NULL END, + items.price_cents + ) * items.quantity + ) FROM setup_items JOIN items ON items.id = setup_items.item_id + LEFT JOIN global_items ON global_items.id = items.global_item_id WHERE setup_items.setup_id = setups.id ), 0)`.as("total_cost"), }) @@ -55,14 +73,31 @@ export async function getSetupWithItems( const itemList = await db .select({ id: items.id, - name: items.name, - weightGrams: items.weightGrams, - priceCents: items.priceCents, + name: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL + THEN ${globalItems.brand} || ' ' || ${globalItems.model} + ELSE ${items.name} + END, + ${items.name} + )`.as("name"), + weightGrams: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END, + ${items.weightGrams} + )`.as("weight_grams"), + priceCents: sql`COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END, + ${items.priceCents} + )`.as("price_cents"), quantity: items.quantity, categoryId: items.categoryId, notes: items.notes, productUrl: items.productUrl, - imageFilename: items.imageFilename, + imageFilename: sql`COALESCE( + ${items.imageFilename}, + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END + )`.as("image_filename"), + globalItemId: items.globalItemId, + purchasePriceCents: items.purchasePriceCents, createdAt: items.createdAt, updatedAt: items.updatedAt, categoryName: categories.name, @@ -72,6 +107,7 @@ export async function getSetupWithItems( .from(setupItems) .innerJoin(items, eq(setupItems.itemId, items.id)) .innerJoin(categories, eq(items.categoryId, categories.id)) + .leftJoin(globalItems, eq(items.globalItemId, globalItems.id)) .where(eq(setupItems.setupId, setupId)); return { ...setup, items: itemList }; diff --git a/src/server/services/totals.service.ts b/src/server/services/totals.service.ts index c120caa..9ce9c38 100644 --- a/src/server/services/totals.service.ts +++ b/src/server/services/totals.service.ts @@ -1,6 +1,6 @@ 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"; type Db = typeof prodDb; @@ -10,12 +10,23 @@ export async function getCategoryTotals(db: Db, userId: number) { categoryId: items.categoryId, categoryName: categories.name, categoryIcon: categories.icon, - totalWeight: sql`COALESCE(SUM(${items.weightGrams} * ${items.quantity}), 0)`, - totalCost: sql`COALESCE(SUM(${items.priceCents} * ${items.quantity}), 0)`, + totalWeight: sql`COALESCE(SUM( + COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END, + ${items.weightGrams} + ) * ${items.quantity} + ), 0)`, + totalCost: sql`COALESCE(SUM( + COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END, + ${items.priceCents} + ) * ${items.quantity} + ), 0)`, itemCount: sql`COUNT(*)`, }) .from(items) .innerJoin(categories, eq(items.categoryId, categories.id)) + .leftJoin(globalItems, eq(items.globalItemId, globalItems.id)) .where(eq(items.userId, userId)) .groupBy(items.categoryId, categories.name, categories.icon); } @@ -23,11 +34,22 @@ export async function getCategoryTotals(db: Db, userId: number) { export async function getGlobalTotals(db: Db, userId: number) { const [row] = await db .select({ - totalWeight: sql`COALESCE(SUM(${items.weightGrams} * ${items.quantity}), 0)`, - totalCost: sql`COALESCE(SUM(${items.priceCents} * ${items.quantity}), 0)`, + totalWeight: sql`COALESCE(SUM( + COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END, + ${items.weightGrams} + ) * ${items.quantity} + ), 0)`, + totalCost: sql`COALESCE(SUM( + COALESCE( + CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END, + ${items.priceCents} + ) * ${items.quantity} + ), 0)`, itemCount: sql`COUNT(*)`, }) .from(items) + .leftJoin(globalItems, eq(items.globalItemId, globalItems.id)) .where(eq(items.userId, userId)); return row;