import { changedColumns, extractCustomFields, pick } from "../../../shared/repository"; import { markInvariantsSatisfied, type OutboundShipment, type OutboundShipmentHeader, type OutboundShipmentLine, } from "../domain/outboundShipment"; import type { Insertable, Selectable, Transaction, Updateable } from "../generated/kysely-tailordb"; export interface OutboundShipmentRepository { findById(id: string, opts?: { forUpdate?: boolean }): Promise; save(shipment: OutboundShipment): Promise; } // ===== Columns ===== /** The domain-owned columns of each table. Everything else round-trips as custom fields. */ const OUTBOUND_SHIPMENT_HEADER_COLUMNS = [ "status", "effectiveDate", "postedAt", ] as const satisfies readonly Exclude[]; const OUTBOUND_SHIPMENT_LINE_COLUMNS = [ "id", "itemId", "quantity", "unitId", "primaryQuantity", "primaryUnitId", "unitConversionRate", "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 = ["outboundShipmentId", "createdAt", "updatedAt"] as const; // ===== Mapping ===== export function toOutboundShipment( headerRow: Selectable<"OutboundShipment">, lineRows: Selectable<"OutboundShipmentLine">[], ): OutboundShipment { const header: OutboundShipmentHeader = { ...pick(headerRow, OUTBOUND_SHIPMENT_HEADER_COLUMNS), customFields: extractCustomFields(headerRow, [ ...OUTBOUND_SHIPMENT_HEADER_COLUMNS, ...HEADER_ROW_ONLY_COLUMNS, ]), }; const lines = lineRows.map((lineRow): OutboundShipmentLine => ({ ...pick(lineRow, OUTBOUND_SHIPMENT_LINE_COLUMNS), customFields: extractCustomFields(lineRow, [ ...OUTBOUND_SHIPMENT_LINE_COLUMNS, ...LINE_ROW_ONLY_COLUMNS, ]), })); return markInvariantsSatisfied({ id: headerRow.id, header, lines }); } /** Custom fields first so domain-owned columns always win. */ function toOutboundShipmentRow(shipment: OutboundShipment) { const row: Record = { ...shipment.header.customFields, id: shipment.id }; for (const column of OUTBOUND_SHIPMENT_HEADER_COLUMNS) { row[column] = shipment.header[column]; } return row; } /** Custom fields first so domain-owned columns always win. */ function toOutboundShipmentLineRow(line: OutboundShipmentLine, outboundShipmentId: string) { const row: Record = { ...line.customFields, outboundShipmentId }; for (const column of OUTBOUND_SHIPMENT_LINE_COLUMNS) { row[column] = line[column]; } return row; } // ===== Repository ===== export function createOutboundShipmentRepository(db: Transaction): OutboundShipmentRepository { return { async findById(id, opts) { let headerQuery = db.selectFrom("OutboundShipment").selectAll().where("id", "=", id); if (opts?.forUpdate) { headerQuery = headerQuery.forUpdate(); } const headerRow = await headerQuery.executeTakeFirst(); if (!headerRow) { return null; } const lineRows = await db .selectFrom("OutboundShipmentLine") .selectAll() .where("outboundShipmentId", "=", id) .execute(); return toOutboundShipment(headerRow, lineRows); }, // Diffing against the stored rows keeps a no-op save from bumping updatedAt. async save(shipment) { const storedHeader = await db .selectFrom("OutboundShipment") .selectAll() .where("id", "=", shipment.id) .forUpdate() .executeTakeFirst(); if (!storedHeader) { await db .insertInto("OutboundShipment") .values(toOutboundShipmentRow(shipment) as Insertable<"OutboundShipment">) .execute(); await db .insertInto("OutboundShipmentLine") .values( shipment.lines.map( (line) => toOutboundShipmentLineRow(line, shipment.id) as Insertable<"OutboundShipmentLine">, ), ) .execute(); return; } const storedLines = await db .selectFrom("OutboundShipmentLine") .selectAll() .where("outboundShipmentId", "=", 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, toOutboundShipmentLineRow(line, shipment.id), ); if (Object.keys(lineChanges).length > 0) { await db .updateTable("OutboundShipmentLine") .set(lineChanges as Updateable<"OutboundShipmentLine">) .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("OutboundShipmentLine").where("id", "in", removedLineIds).execute(); } const addedRows = shipment.lines .filter((line) => !storedLineById.has(line.id)) .map( (line) => toOutboundShipmentLineRow(line, shipment.id) as Insertable<"OutboundShipmentLine">, ); if (addedRows.length > 0) { await db.insertInto("OutboundShipmentLine").values(addedRows).execute(); } const headerChanges = changedColumns(storedHeader, toOutboundShipmentRow(shipment)); if (Object.keys(headerChanges).length > 0) { await db .updateTable("OutboundShipment") .set(headerChanges as Updateable<"OutboundShipment">) .where("id", "=", shipment.id) .execute(); } }, }; }