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
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 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";
|
import { getOrCreateUncategorized } from "./category.service.ts";
|
||||||
|
|
||||||
type Db = typeof prodDb;
|
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<string> {
|
export async function exportItemsCsv(db: Db, userId: number): Promise<string> {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
name: items.name,
|
name: sql<string>`COALESCE(
|
||||||
|
CASE WHEN ${items.globalItemId} IS NOT NULL
|
||||||
|
THEN ${globalItems.brand} || ' ' || ${globalItems.model}
|
||||||
|
ELSE ${items.name}
|
||||||
|
END,
|
||||||
|
${items.name}
|
||||||
|
)`.as("name"),
|
||||||
quantity: items.quantity,
|
quantity: items.quantity,
|
||||||
weightGrams: items.weightGrams,
|
weightGrams: sql<number | null>`COALESCE(
|
||||||
priceCents: items.priceCents,
|
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"),
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
notes: items.notes,
|
notes: items.notes,
|
||||||
productUrl: items.productUrl,
|
productUrl: items.productUrl,
|
||||||
})
|
})
|
||||||
.from(items)
|
.from(items)
|
||||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(eq(items.userId, userId));
|
.where(eq(items.userId, userId));
|
||||||
|
|
||||||
const header =
|
const header =
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { and, eq, sql } from "drizzle-orm";
|
|||||||
import type { db as prodDb } from "../../db/index.ts";
|
import type { db as prodDb } from "../../db/index.ts";
|
||||||
import {
|
import {
|
||||||
categories,
|
categories,
|
||||||
|
globalItems,
|
||||||
items,
|
items,
|
||||||
setupItems,
|
setupItems,
|
||||||
setups,
|
setups,
|
||||||
@@ -55,13 +56,25 @@ export async function getPublicProfile(db: Db, userId: number) {
|
|||||||
WHERE setup_items.setup_id = setups.id
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("item_count"),
|
), 0)`.as("item_count"),
|
||||||
totalWeight: sql<number>`COALESCE((
|
totalWeight: sql<number>`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
|
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
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("total_weight"),
|
), 0)`.as("total_weight"),
|
||||||
totalCost: sql<number>`COALESCE((
|
totalCost: sql<number>`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
|
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
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("total_cost"),
|
), 0)`.as("total_cost"),
|
||||||
})
|
})
|
||||||
@@ -82,14 +95,30 @@ export async function getPublicSetupWithItems(db: Db, setupId: number) {
|
|||||||
const itemList = await db
|
const itemList = await db
|
||||||
.select({
|
.select({
|
||||||
id: items.id,
|
id: items.id,
|
||||||
name: items.name,
|
name: sql<string>`COALESCE(
|
||||||
weightGrams: items.weightGrams,
|
CASE WHEN ${items.globalItemId} IS NOT NULL
|
||||||
priceCents: items.priceCents,
|
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"),
|
||||||
quantity: items.quantity,
|
quantity: items.quantity,
|
||||||
categoryId: items.categoryId,
|
categoryId: items.categoryId,
|
||||||
notes: items.notes,
|
notes: items.notes,
|
||||||
productUrl: items.productUrl,
|
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"),
|
||||||
|
globalItemId: items.globalItemId,
|
||||||
createdAt: items.createdAt,
|
createdAt: items.createdAt,
|
||||||
updatedAt: items.updatedAt,
|
updatedAt: items.updatedAt,
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
@@ -99,6 +128,7 @@ export async function getPublicSetupWithItems(db: Db, setupId: number) {
|
|||||||
.from(setupItems)
|
.from(setupItems)
|
||||||
.innerJoin(items, eq(setupItems.itemId, items.id))
|
.innerJoin(items, eq(setupItems.itemId, items.id))
|
||||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(eq(setupItems.setupId, setupId));
|
.where(eq(setupItems.setupId, setupId));
|
||||||
|
|
||||||
return { ...setup, items: itemList };
|
return { ...setup, items: itemList };
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import type { db as prodDb } from "../../db/index.ts";
|
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";
|
import type { CreateSetup, UpdateSetup } from "../../shared/types.ts";
|
||||||
|
|
||||||
type Db = typeof prodDb;
|
type Db = typeof prodDb;
|
||||||
@@ -27,13 +33,25 @@ export async function getAllSetups(db: Db, userId: number) {
|
|||||||
WHERE setup_items.setup_id = setups.id
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("item_count"),
|
), 0)`.as("item_count"),
|
||||||
totalWeight: sql<number>`COALESCE((
|
totalWeight: sql<number>`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
|
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
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("total_weight"),
|
), 0)`.as("total_weight"),
|
||||||
totalCost: sql<number>`COALESCE((
|
totalCost: sql<number>`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
|
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
|
WHERE setup_items.setup_id = setups.id
|
||||||
), 0)`.as("total_cost"),
|
), 0)`.as("total_cost"),
|
||||||
})
|
})
|
||||||
@@ -55,14 +73,31 @@ export async function getSetupWithItems(
|
|||||||
const itemList = await db
|
const itemList = await db
|
||||||
.select({
|
.select({
|
||||||
id: items.id,
|
id: items.id,
|
||||||
name: items.name,
|
name: sql<string>`COALESCE(
|
||||||
weightGrams: items.weightGrams,
|
CASE WHEN ${items.globalItemId} IS NOT NULL
|
||||||
priceCents: items.priceCents,
|
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"),
|
||||||
quantity: items.quantity,
|
quantity: items.quantity,
|
||||||
categoryId: items.categoryId,
|
categoryId: items.categoryId,
|
||||||
notes: items.notes,
|
notes: items.notes,
|
||||||
productUrl: items.productUrl,
|
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"),
|
||||||
|
globalItemId: items.globalItemId,
|
||||||
|
purchasePriceCents: items.purchasePriceCents,
|
||||||
createdAt: items.createdAt,
|
createdAt: items.createdAt,
|
||||||
updatedAt: items.updatedAt,
|
updatedAt: items.updatedAt,
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
@@ -72,6 +107,7 @@ export async function getSetupWithItems(
|
|||||||
.from(setupItems)
|
.from(setupItems)
|
||||||
.innerJoin(items, eq(setupItems.itemId, items.id))
|
.innerJoin(items, eq(setupItems.itemId, items.id))
|
||||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(eq(setupItems.setupId, setupId));
|
.where(eq(setupItems.setupId, setupId));
|
||||||
|
|
||||||
return { ...setup, items: itemList };
|
return { ...setup, items: itemList };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
import type { db as prodDb } from "../../db/index.ts";
|
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;
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
@@ -10,12 +10,23 @@ export async function getCategoryTotals(db: Db, userId: number) {
|
|||||||
categoryId: items.categoryId,
|
categoryId: items.categoryId,
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
categoryIcon: categories.icon,
|
categoryIcon: categories.icon,
|
||||||
totalWeight: sql<number>`COALESCE(SUM(${items.weightGrams} * ${items.quantity}), 0)`,
|
totalWeight: sql<number>`COALESCE(SUM(
|
||||||
totalCost: sql<number>`COALESCE(SUM(${items.priceCents} * ${items.quantity}), 0)`,
|
COALESCE(
|
||||||
|
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
|
||||||
|
${items.weightGrams}
|
||||||
|
) * ${items.quantity}
|
||||||
|
), 0)`,
|
||||||
|
totalCost: sql<number>`COALESCE(SUM(
|
||||||
|
COALESCE(
|
||||||
|
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
|
||||||
|
${items.priceCents}
|
||||||
|
) * ${items.quantity}
|
||||||
|
), 0)`,
|
||||||
itemCount: sql<number>`COUNT(*)`,
|
itemCount: sql<number>`COUNT(*)`,
|
||||||
})
|
})
|
||||||
.from(items)
|
.from(items)
|
||||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(eq(items.userId, userId))
|
.where(eq(items.userId, userId))
|
||||||
.groupBy(items.categoryId, categories.name, categories.icon);
|
.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) {
|
export async function getGlobalTotals(db: Db, userId: number) {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({
|
.select({
|
||||||
totalWeight: sql<number>`COALESCE(SUM(${items.weightGrams} * ${items.quantity}), 0)`,
|
totalWeight: sql<number>`COALESCE(SUM(
|
||||||
totalCost: sql<number>`COALESCE(SUM(${items.priceCents} * ${items.quantity}), 0)`,
|
COALESCE(
|
||||||
|
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
|
||||||
|
${items.weightGrams}
|
||||||
|
) * ${items.quantity}
|
||||||
|
), 0)`,
|
||||||
|
totalCost: sql<number>`COALESCE(SUM(
|
||||||
|
COALESCE(
|
||||||
|
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
|
||||||
|
${items.priceCents}
|
||||||
|
) * ${items.quantity}
|
||||||
|
), 0)`,
|
||||||
itemCount: sql<number>`COUNT(*)`,
|
itemCount: sql<number>`COUNT(*)`,
|
||||||
})
|
})
|
||||||
.from(items)
|
.from(items)
|
||||||
|
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
|
||||||
.where(eq(items.userId, userId));
|
.where(eq(items.userId, userId));
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
|
|||||||
Reference in New Issue
Block a user