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