import { changedColumns, extractCustomFields, pick } from "../../../shared/repository"; import { markInvariantsSatisfied, type InboundShipment, type InboundShipmentHeader, type InboundShipmentLine, } from "../domain/inboundShipment"; import type { Insertable, Selectable, Transaction, Updateable } from "../generated/kysely-tailordb"; export interface InboundShipmentRepository { findById(id: string, opts?: { forUpdate?: boolean }): Promise; save(shipment: InboundShipment): Promise; } // ===== Columns ===== /** The domain-owned columns of each table. Everything else round-trips as custom fields. */ const INBOUND_SHIPMENT_HEADER_COLUMNS = [ "status", "effectiveDate", "postedAt", ] as const satisfies readonly Exclude[]; const INBOUND_SHIPMENT_LINE_COLUMNS = [ "id", "itemId", "quantity", "unitId", "primaryQuantity", "primaryUnitId", "unitConversionRate", "unitCost", "storageLocationId", "stockType", "sourceDocumentType", "sourceDocumentId", "sourceLineId", ] as const satisfies readonly Exclude[]; const HEADER_ROW_ONLY_COLUMNS = ["id", "createdAt", "updatedAt"] as const; const LINE_ROW_ONLY_COLUMNS = ["inboundShipmentId", "createdAt", "updatedAt"] as const; // ===== Mapping ===== export function toInboundShipment( headerRow: Selectable<"InboundShipment">, lineRows: Selectable<"InboundShipmentLine">[], ): InboundShipment { const header: InboundShipmentHeader = { ...pick(headerRow, INBOUND_SHIPMENT_HEADER_COLUMNS), customFields: extractCustomFields(headerRow, [ ...INBOUND_SHIPMENT_HEADER_COLUMNS, ...HEADER_ROW_ONLY_COLUMNS, ]), }; const lines = lineRows.map((lineRow): InboundShipmentLine => ({ ...pick(lineRow, INBOUND_SHIPMENT_LINE_COLUMNS), customFields: extractCustomFields(lineRow, [ ...INBOUND_SHIPMENT_LINE_COLUMNS, ...LINE_ROW_ONLY_COLUMNS, ]), })); return markInvariantsSatisfied({ id: headerRow.id, header, lines }); } /** Custom fields first so domain-owned columns always win. */ function toInboundShipmentRow(shipment: InboundShipment) { const row: Record = { ...shipment.header.customFields, id: shipment.id }; for (const column of INBOUND_SHIPMENT_HEADER_COLUMNS) { row[column] = shipment.header[column]; } return row; } /** Custom fields first so domain-owned columns always win. */ function toInboundShipmentLineRow(line: InboundShipmentLine, inboundShipmentId: string) { const row: Record = { ...line.customFields, inboundShipmentId }; for (const column of INBOUND_SHIPMENT_LINE_COLUMNS) { row[column] = line[column]; } return row; } // ===== Repository ===== export function createInboundShipmentRepository(db: Transaction): InboundShipmentRepository { return { async findById(id, opts) { let headerQuery = db.selectFrom("InboundShipment").selectAll().where("id", "=", id); if (opts?.forUpdate) { headerQuery = headerQuery.forUpdate(); } const headerRow = await headerQuery.executeTakeFirst(); if (!headerRow) { return null; } const lineRows = await db .selectFrom("InboundShipmentLine") .selectAll() .where("inboundShipmentId", "=", id) .execute(); return toInboundShipment(headerRow, lineRows); }, // Diffing against the stored rows keeps a no-op save from bumping updatedAt. async save(shipment) { const storedHeader = await db .selectFrom("InboundShipment") .selectAll() .where("id", "=", shipment.id) .forUpdate() .executeTakeFirst(); if (!storedHeader) { await db .insertInto("InboundShipment") .values(toInboundShipmentRow(shipment) as Insertable<"InboundShipment">) .execute(); await db .insertInto("InboundShipmentLine") .values( shipment.lines.map( (line) => toInboundShipmentLineRow(line, shipment.id) as Insertable<"InboundShipmentLine">, ), ) .execute(); return; } const storedLines = await db .selectFrom("InboundShipmentLine") .selectAll() .where("inboundShipmentId", "=", shipment.id) .execute(); const storedLineById = new Map(storedLines.map((lineRow) => [lineRow.id, lineRow])); for (const line of shipment.lines) { const storedLine = storedLineById.get(line.id); if (!storedLine) { continue; } const lineChanges = changedColumns(storedLine, toInboundShipmentLineRow(line, shipment.id)); if (Object.keys(lineChanges).length > 0) { await db .updateTable("InboundShipmentLine") .set(lineChanges as Updateable<"InboundShipmentLine">) .where("id", "=", line.id) .execute(); } } const keptLineIds = new Set(shipment.lines.map((line) => line.id)); const removedLineIds = storedLines .filter((lineRow) => !keptLineIds.has(lineRow.id)) .map((lineRow) => lineRow.id); if (removedLineIds.length > 0) { await db.deleteFrom("InboundShipmentLine").where("id", "in", removedLineIds).execute(); } const addedRows = shipment.lines .filter((line) => !storedLineById.has(line.id)) .map( (line) => toInboundShipmentLineRow(line, shipment.id) as Insertable<"InboundShipmentLine">, ); if (addedRows.length > 0) { await db.insertInto("InboundShipmentLine").values(addedRows).execute(); } const headerChanges = changedColumns(storedHeader, toInboundShipmentRow(shipment)); if (Object.keys(headerChanges).length > 0) { await db .updateTable("InboundShipment") .set(headerChanges as Updateable<"InboundShipment">) .where("id", "=", shipment.id) .execute(); } }, }; }