import { changedColumns, extractCustomFields, pick } from "../../../shared/repository"; import { markInvariantsSatisfied, type SalesItem } from "../domain/salesItem"; import type { Insertable, Selectable, Transaction, Updateable } from "../generated/kysely-tailordb"; export interface SalesItemRepository { findByItemId(itemId: string, opts?: { forUpdate?: boolean }): Promise; /** Keyed by itemId; items with no selling record are absent from the map. */ findByItemIds(itemIds: string[]): Promise>; save(salesItem: SalesItem): Promise; deleteByItemId(itemId: string): Promise; } // ===== Columns ===== /** The domain-owned columns. Everything else round-trips as custom fields. */ const SALES_ITEM_COLUMNS = [ "id", "itemId", "requiresPhysicalFulfillment", ] as const satisfies readonly Exclude[]; const ROW_ONLY_COLUMNS = ["createdAt", "updatedAt"] as const; // ===== Mapping ===== export function toSalesItem(row: Selectable<"SalesItem">): SalesItem { return markInvariantsSatisfied({ ...pick(row, SALES_ITEM_COLUMNS), customFields: extractCustomFields(row, [...SALES_ITEM_COLUMNS, ...ROW_ONLY_COLUMNS]), }); } /** Custom fields first so domain-owned columns always win. */ function toSalesItemRow(salesItem: SalesItem) { const row: Record = { ...salesItem.customFields }; for (const column of SALES_ITEM_COLUMNS) { row[column] = salesItem[column]; } return row; } // ===== Repository ===== export function createSalesItemRepository(db: Transaction): SalesItemRepository { return { async findByItemId(itemId, opts) { let query = db.selectFrom("SalesItem").selectAll().where("itemId", "=", itemId); if (opts?.forUpdate) { query = query.forUpdate(); } const row = await query.executeTakeFirst(); return row ? toSalesItem(row) : null; }, async findByItemIds(itemIds) { if (itemIds.length === 0) { return new Map(); } const rows = await db .selectFrom("SalesItem") .selectAll() .where("itemId", "in", [...new Set(itemIds)]) .execute(); return new Map(rows.map((row) => [row.itemId, toSalesItem(row)])); }, // Diffing against the stored row keeps a no-op save from bumping updatedAt. async save(salesItem) { const storedRow = await db .selectFrom("SalesItem") .selectAll() .where("id", "=", salesItem.id) .forUpdate() .executeTakeFirst(); if (!storedRow) { await db .insertInto("SalesItem") .values(toSalesItemRow(salesItem) as Insertable<"SalesItem">) .execute(); return; } const changes = changedColumns(storedRow, toSalesItemRow(salesItem)); if (Object.keys(changes).length === 0) { return; } await db .updateTable("SalesItem") .set(changes as Updateable<"SalesItem">) .where("id", "=", salesItem.id) .execute(); }, async deleteByItemId(itemId) { await db.deleteFrom("SalesItem").where("itemId", "=", itemId).execute(); }, }; }