diff --git a/src/controllers/asset.ts b/src/controllers/asset.ts new file mode 100644 index 0000000..b894e5c --- /dev/null +++ b/src/controllers/asset.ts @@ -0,0 +1,16 @@ +import { deleteAssetService, findSingleAssetService } from '#src/services/index'; + +import type { Request, Response } from 'express'; +import type { SystemRecordQuery } from '#types'; + +export const deleteAssetController = async ( + req: Request>, + res: Response +): Promise => { + await findSingleAssetService(req.query.record_id, req.query.system_id); + await deleteAssetService(req.query.record_id, req.query.system_id); + res.status(204).end(); +}; + +// Legacy alias for backwards compatibility +export const deleteSystemRecordController = deleteAssetController; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 453ad4f..42819d1 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ export * from './record.ts'; -export * from './systemRecord.ts'; +export * from './asset.ts'; export * from './validate.ts'; diff --git a/src/controllers/systemRecord.ts b/src/controllers/systemRecord.ts index 0164620..b0e48f2 100644 --- a/src/controllers/systemRecord.ts +++ b/src/controllers/systemRecord.ts @@ -1,4 +1,4 @@ -import { deleteSystemRecordService, findSingleSystemRecordService } from '#src/services/index'; +import { deleteAssetService, findSingleAssetService } from '#src/services/index'; import type { Request, Response } from 'express'; import type { SystemRecordQuery } from '#types'; @@ -7,7 +7,7 @@ export const deleteSystemRecordController = async ( req: Request>, res: Response ): Promise => { - await findSingleSystemRecordService(req.query.record_id, req.query.system_id); - await deleteSystemRecordService(req.query.record_id, req.query.system_id); + await findSingleAssetService(req.query.record_id, req.query.system_id); + await deleteAssetService(req.query.record_id, req.query.system_id); res.status(204).end(); }; diff --git a/src/db/migrations/1781723179517_rename_system_record_to_asset.ts b/src/db/migrations/1781723179517_rename_system_record_to_asset.ts new file mode 100644 index 0000000..014b60e --- /dev/null +++ b/src/db/migrations/1781723179517_rename_system_record_to_asset.ts @@ -0,0 +1,224 @@ +import { sql } from 'kysely'; + +import type { Kysely } from 'kysely'; + +import { + createAuditLogTrigger, + createIndex, + createUpdatedAtTrigger, + dropAuditLogTrigger, + dropIndex, + dropUpdatedAtTrigger +} from '#src/db/index'; + +// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface. +/** + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function up(db: Kysely): Promise { + // Explicitly drop system_record table triggers first using raw SQL + await sql`DROP TRIGGER IF EXISTS pies_system_record_bu_trigger ON pies.system_record`.execute(db); + await sql`DROP TRIGGER IF EXISTS audit_system_record_au_trigger ON pies.system_record`.execute(db); + + // Drop dependent table triggers (in reverse order of creation) + await dropAuditLogTrigger(db, 'pies', 'record_linkage'); + await dropUpdatedAtTrigger(db, 'pies', 'record_linkage'); + await dropAuditLogTrigger(db, 'pies', 'process_event'); + await dropUpdatedAtTrigger(db, 'pies', 'process_event'); + await dropAuditLogTrigger(db, 'pies', 'on_hold_event'); + await dropUpdatedAtTrigger(db, 'pies', 'on_hold_event'); + + // Drop indices in dependent tables before renaming columns + await dropIndex(db, 'pies', 'on_hold_event', ['system_record_id']); + await dropIndex(db, 'pies', 'process_event', ['system_record_id']); + await dropIndex(db, 'pies', 'record_linkage', ['system_record_id']); + await dropIndex(db, 'pies', 'record_linkage', ['linked_system_record_id']); + + // Drop the special undirected index on record_linkage (handles bidirectional links) + await sql`DROP INDEX IF EXISTS pies.record_linkage_undirected`.execute(db); + + // Drop old indices on system_record table + await dropIndex(db, 'pies', 'system_record', ['record_id']); + await dropIndex(db, 'pies', 'system_record', ['system_id']); + + // Rename columns in dependent tables: system_record_id → asset_id + await sql`ALTER TABLE pies.on_hold_event RENAME COLUMN system_record_id TO asset_id`.execute(db); + await sql`ALTER TABLE pies.process_event RENAME COLUMN system_record_id TO asset_id`.execute(db); + await sql`ALTER TABLE pies.record_linkage RENAME COLUMN system_record_id TO asset_id`.execute(db); + await sql`ALTER TABLE pies.record_linkage RENAME COLUMN linked_system_record_id TO linked_asset_id`.execute(db); + + // Rename foreign key constraints in dependent tables + await sql` + ALTER TABLE pies.on_hold_event + RENAME CONSTRAINT on_hold_event_system_record_id_fkey + TO on_hold_event_asset_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.process_event + RENAME CONSTRAINT process_event_system_record_id_fkey + TO process_event_asset_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.record_linkage + RENAME CONSTRAINT record_linkage_system_record_id_fkey + TO record_linkage_asset_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.record_linkage + RENAME CONSTRAINT record_linkage_linked_system_record_id_fkey + TO record_linkage_linked_asset_id_fkey + `.execute(db); + + // Rename the main table and its constraints + await sql`ALTER TABLE pies.system_record RENAME TO asset`.execute(db); + await sql` + ALTER TABLE pies.asset + RENAME CONSTRAINT system_record_system_id_record_id_unique + TO asset_system_id_record_id_unique + `.execute(db); + + // Recreate indices in dependent tables with new column names + await createIndex(db, 'pies', 'on_hold_event', ['asset_id']); + await createIndex(db, 'pies', 'process_event', ['asset_id']); + await createIndex(db, 'pies', 'record_linkage', ['asset_id']); + await createIndex(db, 'pies', 'record_linkage', ['linked_asset_id']); + + // Recreate the special undirected index on record_linkage with new column names + await sql` + CREATE UNIQUE INDEX record_linkage_undirected + ON pies.record_linkage ( + LEAST(asset_id, linked_asset_id), + GREATEST(asset_id, linked_asset_id) + ); + `.execute(db); + + // Recreate indices on asset table (using new table name) + await createIndex(db, 'pies', 'asset', ['record_id']); + await createIndex(db, 'pies', 'asset', ['system_id']); + + // Recreate triggers for dependent tables + await createUpdatedAtTrigger(db, 'pies', 'on_hold_event'); + await createAuditLogTrigger(db, 'pies', 'on_hold_event'); + await createUpdatedAtTrigger(db, 'pies', 'process_event'); + await createAuditLogTrigger(db, 'pies', 'process_event'); + await createUpdatedAtTrigger(db, 'pies', 'record_linkage'); + await createAuditLogTrigger(db, 'pies', 'record_linkage'); + + // Recreate triggers on asset table + await createUpdatedAtTrigger(db, 'pies', 'asset'); + await createAuditLogTrigger(db, 'pies', 'asset'); + + // Update the audit log table to track the rename (optional audit event) + await sql` + INSERT INTO audit.logged_actions ( + "schema_name", "table_name", "db_user", "action_timestamp", "action", "original_data", "new_data" + ) + VALUES ( + 'pies', 'asset', SESSION_USER, now(), 'TABLE_RENAME', '{"table":"system_record"}', '{"table":"asset"}' + ) + `.execute(db); +} + +// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface. +/** + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function down(db: Kysely): Promise { + // Drop dependent table triggers (in reverse order) + await dropAuditLogTrigger(db, 'pies', 'record_linkage'); + await dropUpdatedAtTrigger(db, 'pies', 'record_linkage'); + await dropAuditLogTrigger(db, 'pies', 'process_event'); + await dropUpdatedAtTrigger(db, 'pies', 'process_event'); + await dropAuditLogTrigger(db, 'pies', 'on_hold_event'); + await dropUpdatedAtTrigger(db, 'pies', 'on_hold_event'); + + // Explicitly drop asset table triggers using raw SQL + await sql`DROP TRIGGER IF EXISTS pies_asset_bu_trigger ON pies.asset`.execute(db); + await sql`DROP TRIGGER IF EXISTS audit_asset_au_trigger ON pies.asset`.execute(db); + + // Drop indices in dependent tables + await dropIndex(db, 'pies', 'on_hold_event', ['asset_id']); + await dropIndex(db, 'pies', 'process_event', ['asset_id']); + await dropIndex(db, 'pies', 'record_linkage', ['asset_id']); + await dropIndex(db, 'pies', 'record_linkage', ['linked_asset_id']); + + // Drop the special undirected index on record_linkage + await sql`DROP INDEX IF EXISTS pies.record_linkage_undirected`.execute(db); + + // Drop indices on asset table + await dropIndex(db, 'pies', 'asset', ['record_id']); + await dropIndex(db, 'pies', 'asset', ['system_id']); + + // Rename the main table back to system_record (do this FIRST, before renaming columns) + await sql`ALTER TABLE pies.asset RENAME TO system_record`.execute(db); + await sql` + ALTER TABLE pies.system_record + RENAME CONSTRAINT asset_system_id_record_id_unique + TO system_record_system_id_record_id_unique + `.execute(db); + + // Now rename columns back: asset_id → system_record_id + await sql`ALTER TABLE pies.on_hold_event RENAME COLUMN asset_id TO system_record_id`.execute(db); + await sql`ALTER TABLE pies.process_event RENAME COLUMN asset_id TO system_record_id`.execute(db); + await sql`ALTER TABLE pies.record_linkage RENAME COLUMN asset_id TO system_record_id`.execute(db); + await sql`ALTER TABLE pies.record_linkage RENAME COLUMN linked_asset_id TO linked_system_record_id`.execute(db); + + // Rename foreign key constraints back + await sql` + ALTER TABLE pies.on_hold_event + RENAME CONSTRAINT on_hold_event_asset_id_fkey + TO on_hold_event_system_record_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.process_event + RENAME CONSTRAINT process_event_asset_id_fkey + TO process_event_system_record_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.record_linkage + RENAME CONSTRAINT record_linkage_asset_id_fkey + TO record_linkage_system_record_id_fkey + `.execute(db); + await sql` + ALTER TABLE pies.record_linkage + RENAME CONSTRAINT record_linkage_linked_asset_id_fkey + TO record_linkage_linked_system_record_id_fkey + `.execute(db); + + // Recreate indices in dependent tables with original column names + await createIndex(db, 'pies', 'on_hold_event', ['system_record_id']); + await createIndex(db, 'pies', 'process_event', ['system_record_id']); + await createIndex(db, 'pies', 'record_linkage', ['system_record_id']); + await createIndex(db, 'pies', 'record_linkage', ['linked_system_record_id']); + + // Recreate the special undirected index on record_linkage with original column names + await sql` + CREATE UNIQUE INDEX record_linkage_undirected + ON pies.record_linkage ( + LEAST(system_record_id, linked_system_record_id), + GREATEST(system_record_id, linked_system_record_id) + ); + `.execute(db); + + // Recreate indices on system_record table + await createIndex(db, 'pies', 'system_record', ['record_id']); + await createIndex(db, 'pies', 'system_record', ['system_id']); + + // Explicitly drop any existing triggers on system_record before recreating + await sql`DROP TRIGGER IF EXISTS pies_system_record_bu_trigger ON pies.system_record`.execute(db); + await sql`DROP TRIGGER IF EXISTS audit_system_record_au_trigger ON pies.system_record`.execute(db); + + // Recreate triggers for dependent tables + await createUpdatedAtTrigger(db, 'pies', 'on_hold_event'); + await createAuditLogTrigger(db, 'pies', 'on_hold_event'); + await createUpdatedAtTrigger(db, 'pies', 'process_event'); + await createAuditLogTrigger(db, 'pies', 'process_event'); + await createUpdatedAtTrigger(db, 'pies', 'record_linkage'); + await createAuditLogTrigger(db, 'pies', 'record_linkage'); + + // Recreate triggers on system_record table + await createUpdatedAtTrigger(db, 'pies', 'system_record'); + await createAuditLogTrigger(db, 'pies', 'system_record'); +} diff --git a/src/repositories/asset.ts b/src/repositories/asset.ts new file mode 100644 index 0000000..9eab774 --- /dev/null +++ b/src/repositories/asset.ts @@ -0,0 +1,12 @@ +import { BaseRepository } from './base.ts'; + +import type { Kysely, Transaction } from 'kysely'; +import type { DB } from '#types'; + +const CONSTRAINTS = ['asset_system_id_record_id_unique'] as const; + +export class AssetRepository extends BaseRepository<'pies.asset', (typeof CONSTRAINTS)[number]> { + constructor(db?: Kysely | Transaction) { + super('pies.asset', db, CONSTRAINTS); + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 754a2ff..b6831db 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -5,6 +5,9 @@ export * from './onHoldEvent.ts'; export * from './processEvent.ts'; export * from './recordLinkage.ts'; export * from './system.ts'; -export * from './systemRecord.ts'; +export * from './asset.ts'; export * from './transaction.ts'; export * from './version.ts'; + +// Legacy alias for backwards compatibility +export { AssetRepository as SystemRecordRepository } from './asset.ts'; diff --git a/src/repositories/onHoldEvent.ts b/src/repositories/onHoldEvent.ts index 8d94e07..d80325a 100644 --- a/src/repositories/onHoldEvent.ts +++ b/src/repositories/onHoldEvent.ts @@ -11,11 +11,11 @@ export class OnHoldEventRepository extends BaseRepository<'pies.onHoldEvent'> { } /** - * Deletes all on hold event entities associated with a specific system record. - * @param systemRecordId - The ID of the system record whose on hold events should be deleted. + * Deletes all on hold event entities associated with a specific asset. + * @param assetId - The ID of the asset whose on hold events should be deleted. * @returns A query builder instance configured to delete the specified records. */ - prune(systemRecordId: number): DeleteQueryBuilder { - return this.db.deleteFrom(this.tableName).where(sql.ref('system_record_id'), '=', systemRecordId); + prune(assetId: number): DeleteQueryBuilder { + return this.db.deleteFrom(this.tableName).where(sql.ref('asset_id'), '=', assetId); } } diff --git a/src/repositories/processEvent.ts b/src/repositories/processEvent.ts index db1fa9e..260c2f9 100644 --- a/src/repositories/processEvent.ts +++ b/src/repositories/processEvent.ts @@ -11,11 +11,11 @@ export class ProcessEventRepository extends BaseRepository<'pies.processEvent'> } /** - * Deletes all process event entities associated with a specific system record. - * @param systemRecordId - The ID of the system record whose process events should be deleted. + * Deletes all process event entities associated with a specific asset. + * @param assetId - The ID of the asset whose process events should be deleted. * @returns A query builder instance configured to delete the specified records. */ - prune(systemRecordId: number): DeleteQueryBuilder { - return this.db.deleteFrom(this.tableName).where(sql.ref('system_record_id'), '=', systemRecordId); + prune(assetId: number): DeleteQueryBuilder { + return this.db.deleteFrom(this.tableName).where(sql.ref('asset_id'), '=', assetId); } } diff --git a/src/repositories/systemRecord.ts b/src/repositories/systemRecord.ts index 33c65ca..1e9f0e7 100644 --- a/src/repositories/systemRecord.ts +++ b/src/repositories/systemRecord.ts @@ -3,10 +3,13 @@ import { BaseRepository } from './base.ts'; import type { Kysely, Transaction } from 'kysely'; import type { DB } from '#types'; -const CONSTRAINTS = ['system_record_system_id_record_id_unique'] as const; +const CONSTRAINTS = ['asset_system_id_record_id_unique'] as const; -export class SystemRecordRepository extends BaseRepository<'pies.systemRecord', (typeof CONSTRAINTS)[number]> { +/** + * @deprecated Use AssetRepository instead + */ +export class SystemRecordRepository extends BaseRepository<'pies.asset', (typeof CONSTRAINTS)[number]> { constructor(db?: Kysely | Transaction) { - super('pies.systemRecord', db, CONSTRAINTS); + super('pies.asset', db, CONSTRAINTS); } } diff --git a/src/services/asset.ts b/src/services/asset.ts new file mode 100644 index 0000000..b658d87 --- /dev/null +++ b/src/services/asset.ts @@ -0,0 +1,57 @@ +import { transactionWrapper } from './helpers/index.ts'; +import { AssetRepository } from '#src/repositories/index'; +import { Problem } from '#src/utils/index'; + +import type { Selectable, SimplifySingleResult } from 'kysely'; +import type { PiesAsset } from '#types'; + +/** + * Deletes a single asset by its record ID and optionally by system ID. + * @param recordId - The unique identifier of the record to find. + * @param systemId - (Optional) The system ID to further filter the search. + * @returns A promise that resolves if the asset is deleted. + * @throws If no asset is found (404) or if multiple assets are found without a `systemId` (409). + */ +export const deleteAssetService = async ( + recordId: string, + systemId?: string +): Promise> => { + return transactionWrapper(async (trx) => { + return await new AssetRepository(trx).deleteWhere({ recordId, systemId }).executeTakeFirst(); + }); +}; + +/** + * Finds a single asset by its record ID and optionally by system ID. + * @param recordId - The unique identifier of the record to find. + * @param systemId - (Optional) The system ID to further filter the search. + * @returns A promise that resolves to the found asset. + * @throws If no asset is found (404) or if multiple assets are found without a `systemId` (409). + */ +export const findSingleAssetService = async (recordId: string, systemId?: string): Promise> => { + return transactionWrapper( + async (trx) => { + const assets = await new AssetRepository(trx).findWhere({ recordId, systemId }).execute(); + + if (!assets.length) { + const params: Record = { record_id: recordId }; + if (systemId) params.system_id = systemId; + throw new Problem(404, { detail: 'Asset not found' }, params); + } + if (assets.length > 1) { + throw new Problem( + 409, + { detail: 'Cannot disambiguate intended asset without `system_id`' }, + { record_id: recordId } + ); + } + + return assets[0]!; + }, + { accessMode: 'read only' } + ); +}; + +// Legacy aliases for backwards compatibility +export const deleteSystemRecordService = deleteAssetService; +export const findSingleSystemRecordService = findSingleAssetService; diff --git a/src/services/index.ts b/src/services/index.ts index 928ec23..bf0f94e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,3 @@ export * from './header.ts'; export * from './record.ts'; -export * from './systemRecord.ts'; +export * from './asset.ts'; diff --git a/src/services/record.ts b/src/services/record.ts index 8e2a42a..af750d2 100644 --- a/src/services/record.ts +++ b/src/services/record.ts @@ -14,48 +14,34 @@ import { ProcessEventRepository, RecordKindRepository, SystemRepository, - SystemRecordRepository, + AssetRepository, TransactionRepository, VersionRepository } from '#src/repositories/index'; import { CodingDictionary, getLogger, Problem, shallowEqual } from '#src/utils/index'; import type { DeleteResult, Selectable } from 'kysely'; -import type { - Coding, - CodingEvent, - Header, - PiesSystemRecord, - Process, - ProcessEvent, - Record as PiesRecord -} from '#types'; +import type { Coding, CodingEvent, Header, PiesAsset, Process, ProcessEvent, Record as PiesRecord } from '#types'; const log = getLogger(import.meta.filename); /** - * Retrieves the record for the given system record. - * @param systemRecord - The system record for which to retrieve the record. - * @returns A Promise that resolves to the record for the given system record. + * Retrieves the record for the given asset. + * @param asset - The asset for which to retrieve the record. + * @returns A Promise that resolves to the record for the given asset. * @throws 404 if no process events are found. */ -export const findRecordService = (systemRecord: Selectable): Promise => { +export const findRecordService = (asset: Selectable): Promise => { return transactionWrapper( async (trx) => { - const recordKind = await cacheableRead(new RecordKindRepository(trx), systemRecord.recordKindId).catch( - (error) => { - log.warn(`No record kind found, ${error}`); - throw new Problem(404, { detail: 'No record kind found.' }); - } - ); + const recordKind = await cacheableRead(new RecordKindRepository(trx), asset.recordKindId).catch((error) => { + log.warn(`No record kind found, ${error}`); + throw new Problem(404, { detail: 'No record kind found.' }); + }); - const processEventsRaw = await new ProcessEventRepository(trx) - .findWhere({ systemRecordId: systemRecord.id }) - .execute(); + const processEventsRaw = await new ProcessEventRepository(trx).findWhere({ assetId: asset.id }).execute(); - const onHoldEventsRaw = await new OnHoldEventRepository(trx) - .findWhere({ systemRecordId: systemRecord.id }) - .execute(); + const onHoldEventsRaw = await new OnHoldEventRepository(trx).findWhere({ assetId: asset.id }).execute(); let onHoldEvents: CodingEvent[] | undefined; if (onHoldEventsRaw.length) { @@ -118,8 +104,8 @@ export const findRecordService = (systemRecord: Selectable): P transaction_id: uuidv7(), version: recordKind.versionId, kind: 'Record', - system_id: systemRecord.systemId, - record_id: systemRecord.recordId, + system_id: asset.systemId, + record_id: asset.recordId, record_kind: recordKind.kind as Header['record_kind'], on_hold_event_set: onHoldEvents, process_event_set: processEvents @@ -135,17 +121,15 @@ export const mergeRecordService = (_data: PiesRecord, _principal?: string): Prom }; /** - * Prunes the record for the given system record. - * @param systemRecord - The system record to prune. + * Prunes the record for the given asset. + * @param asset - The asset to prune. * @returns A Promise that resolves when the operation is complete. */ -export const pruneRecordService = async ( - systemRecord: Selectable -): Promise => { +export const pruneRecordService = async (asset: Selectable): Promise => { return transactionWrapper(async (trx) => { return await Promise.all([ - new OnHoldEventRepository(trx).prune(systemRecord.id).execute(), - new ProcessEventRepository(trx).prune(systemRecord.id).execute() + new OnHoldEventRepository(trx).prune(asset.id).execute(), + new ProcessEventRepository(trx).prune(asset.id).execute() ]); }); }; @@ -169,25 +153,23 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom kind: data.record_kind, versionId: data.version }); - const systemRecord = await findWhereOrUpsert(new SystemRecordRepository(trx), { + const asset = await findWhereOrUpsert(new AssetRepository(trx), { recordId: data.record_id, recordKindId: recordKind.id, systemId: data.system_id }); // Calculate on hold events - const oheDb = (await new OnHoldEventRepository(trx).findWhere({ systemRecordId: systemRecord.id }).execute()).map( - (ohe) => ({ - id: ohe.id, - codingId: ohe.codingId, - ...dateTimePartsToEvent({ - startDate: ohe.startDate, - startTime: ohe.startTime ?? undefined, - endDate: ohe.endDate ?? undefined, - endTime: ohe.endTime ?? undefined - }) + const oheDb = (await new OnHoldEventRepository(trx).findWhere({ assetId: asset.id }).execute()).map((ohe) => ({ + id: ohe.id, + codingId: ohe.codingId, + ...dateTimePartsToEvent({ + startDate: ohe.startDate, + startTime: ohe.startTime ?? undefined, + endDate: ohe.endDate ?? undefined, + endTime: ohe.endTime ?? undefined }) - ); + })); // Map to a union type of number | object const oheResults = await Promise.all( @@ -212,7 +194,7 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom if (oheMatched) return oheMatched.id; return { codingId, - systemRecordId: systemRecord.id, + assetId: asset.id, transactionId: data.transaction_id, ...eventToDateTimeParts(ce.event), createdBy: principal @@ -224,21 +206,19 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom const oheAdd = oheResults.filter((r) => typeof r !== 'number'); // Calculate process events - const peDb = (await new ProcessEventRepository(trx).findWhere({ systemRecordId: systemRecord.id }).execute()).map( - (pe) => ({ - id: pe.id, - codingId: pe.codingId, - status: pe.status, - statusCode: pe.statusCode, - statusDescription: pe.statusDescription, - ...dateTimePartsToEvent({ - startDate: pe.startDate, - startTime: pe.startTime ?? undefined, - endDate: pe.endDate ?? undefined, - endTime: pe.endTime ?? undefined - }) + const peDb = (await new ProcessEventRepository(trx).findWhere({ assetId: asset.id }).execute()).map((pe) => ({ + id: pe.id, + codingId: pe.codingId, + status: pe.status, + statusCode: pe.statusCode, + statusDescription: pe.statusDescription, + ...dateTimePartsToEvent({ + startDate: pe.startDate, + startTime: pe.startTime ?? undefined, + endDate: pe.endDate ?? undefined, + endTime: pe.endTime ?? undefined }) - ); + })); // Map to a union type: number (matched ID) | object (new record) const peResults = await Promise.all( @@ -281,7 +261,7 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom status: pe.process.status, statusCode: pe.process.status_code, statusDescription: pe.process.status_description, - systemRecordId: systemRecord.id, + assetId: asset.id, transactionId: data.transaction_id, ...eventToDateTimeParts(pe.event), createdBy: principal @@ -295,9 +275,9 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom // Update event tables await Promise.all([ oheMatchedIds.length < oheDb.length && - new OnHoldEventRepository(trx).deleteExcept(oheMatchedIds, { systemRecordId: systemRecord.id }).execute(), + new OnHoldEventRepository(trx).deleteExcept(oheMatchedIds, { assetId: asset.id }).execute(), peMatchedIds.length < peDb.length && - new ProcessEventRepository(trx).deleteExcept(peMatchedIds, { systemRecordId: systemRecord.id }).execute() + new ProcessEventRepository(trx).deleteExcept(peMatchedIds, { assetId: asset.id }).execute() ]); await Promise.all([ oheAdd.length && new OnHoldEventRepository(trx).createMany(oheAdd).execute(), diff --git a/src/services/systemRecord.ts b/src/services/systemRecord.ts index 59edb4f..eba854e 100644 --- a/src/services/systemRecord.ts +++ b/src/services/systemRecord.ts @@ -1,55 +1,55 @@ import { transactionWrapper } from './helpers/index.ts'; -import { SystemRecordRepository } from '#src/repositories/index'; +import { AssetRepository } from '#src/repositories/index'; import { Problem } from '#src/utils/index'; import type { Selectable, SimplifySingleResult } from 'kysely'; -import type { PiesSystemRecord } from '#types'; +import type { PiesAsset } from '#types'; /** - * Deletes a single system record by its record ID and optionally by system ID. + * Deletes a single asset by its record ID and optionally by system ID. * @param recordId - The unique identifier of the record to find. * @param systemId - (Optional) The system ID to further filter the search. - * @returns A promise that resolves if the system record is deleted. - * @throws If no record is found (404) or if multiple records are found without a `systemId` (409). + * @returns A promise that resolves if the asset is deleted. + * @throws If no asset is found (404) or if multiple assets are found without a `systemId` (409). */ export const deleteSystemRecordService = async ( recordId: string, systemId?: string -): Promise> => { +): Promise> => { return transactionWrapper(async (trx) => { - return await new SystemRecordRepository(trx).deleteWhere({ recordId, systemId }).executeTakeFirst(); + return await new AssetRepository(trx).deleteWhere({ recordId, systemId }).executeTakeFirst(); }); }; /** - * Finds a single system record by its record ID and optionally by system ID. + * Finds a single asset by its record ID and optionally by system ID. * @param recordId - The unique identifier of the record to find. * @param systemId - (Optional) The system ID to further filter the search. - * @returns A promise that resolves to the found system record. - * @throws If no record is found (404) or if multiple records are found without a `systemId` (409). + * @returns A promise that resolves to the found asset. + * @throws If no asset is found (404) or if multiple assets are found without a `systemId` (409). */ export const findSingleSystemRecordService = async ( recordId: string, systemId?: string -): Promise> => { +): Promise> => { return transactionWrapper( async (trx) => { - const systemRecords = await new SystemRecordRepository(trx).findWhere({ recordId, systemId }).execute(); + const assets = await new AssetRepository(trx).findWhere({ recordId, systemId }).execute(); - if (!systemRecords.length) { + if (!assets.length) { const params: Record = { record_id: recordId }; if (systemId) params.system_id = systemId; - throw new Problem(404, { detail: 'System record not found' }, params); + throw new Problem(404, { detail: 'Asset not found' }, params); } - if (systemRecords.length > 1) { + if (assets.length > 1) { throw new Problem( 409, - { detail: 'Cannot disambiguate intended system record without `system_id`' }, + { detail: 'Cannot disambiguate intended asset without `system_id`' }, { record_id: recordId } ); } - return systemRecords[0]!; + return assets[0]!; }, { accessMode: 'read only' } ); diff --git a/src/types/models.d.ts b/src/types/models.d.ts index 5c94214..18f2e66 100644 --- a/src/types/models.d.ts +++ b/src/types/models.d.ts @@ -22,6 +22,7 @@ export interface PiesCoding { } export interface PiesOnHoldEvent { + assetId: number; codingId: number; createdAt: Generated; createdBy: Generated; @@ -30,13 +31,13 @@ export interface PiesOnHoldEvent { id: Generated; startDate: Timestamp; startTime: string | null; - systemRecordId: number; transactionId: string; updatedAt: Timestamp | null; updatedBy: string | null; } export interface PiesProcessEvent { + assetId: number; codingId: number; createdAt: Generated; createdBy: Generated; @@ -48,7 +49,6 @@ export interface PiesProcessEvent { status: string | null; statusCode: string | null; statusDescription: string | null; - systemRecordId: number; transactionId: string; updatedAt: Timestamp | null; updatedBy: string | null; @@ -65,11 +65,11 @@ export interface PiesRecordKind { } export interface PiesRecordLinkage { + assetId: number; createdAt: Generated; createdBy: Generated; id: Generated; - linkedSystemRecordId: number; - systemRecordId: number; + linkedAssetId: number; transactionId: string; updatedAt: Timestamp | null; updatedBy: string | null; @@ -83,7 +83,7 @@ export interface PiesSystem { updatedBy: string | null; } -export interface PiesSystemRecord { +export interface PiesAsset { createdAt: Generated; createdBy: Generated; id: Generated; @@ -112,12 +112,12 @@ export interface PiesVersion { export interface DB { 'pies.coding': PiesCoding; + 'pies.asset': PiesAsset; 'pies.onHoldEvent': PiesOnHoldEvent; 'pies.processEvent': PiesProcessEvent; 'pies.recordKind': PiesRecordKind; 'pies.recordLinkage': PiesRecordLinkage; 'pies.system': PiesSystem; - 'pies.systemRecord': PiesSystemRecord; 'pies.transaction': PiesTransaction; 'pies.version': PiesVersion; } diff --git a/tests/unit/controllers/record.test.ts b/tests/unit/controllers/record.test.ts index 5a858b9..4b5417a 100644 --- a/tests/unit/controllers/record.test.ts +++ b/tests/unit/controllers/record.test.ts @@ -11,7 +11,7 @@ import * as services from '#src/services/index'; import { Problem } from '#src/utils/index'; import type { Selectable } from 'kysely'; -import type { PiesSystemRecord, Record } from '#types'; +import type { PiesAsset, Record } from '#types'; describe('Record Controllers', () => { const checkDuplicateTransactionHeaderServiceSpy = vi.spyOn(services, 'checkDuplicateTransactionHeaderService'); @@ -20,7 +20,7 @@ describe('Record Controllers', () => { const pruneRecordServiceSpy = vi.spyOn(services, 'pruneRecordService'); const replaceRecordServiceSpy = vi.spyOn(services, 'replaceRecordService'); - const fakeSystemRecord = { id: 1 } as Selectable; + const fakeAsset = { id: 1 } as Selectable; const fakeResult = {} as Record; let app: express.Application; @@ -36,25 +36,25 @@ describe('Record Controllers', () => { describe('DELETE /records', () => { it('should call services and respond with 204', async () => { - findSingleSystemRecordServiceSpy.mockResolvedValue(fakeSystemRecord); + findSingleSystemRecordServiceSpy.mockResolvedValue(fakeAsset); pruneRecordServiceSpy.mockResolvedValue([]); await request(app).delete('/records').query({ record_id: 'rec1', system_id: 'sys1' }).expect(204); expect(findSingleSystemRecordServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); - expect(pruneRecordServiceSpy).toHaveBeenCalledWith(fakeSystemRecord); + expect(pruneRecordServiceSpy).toHaveBeenCalledWith(fakeAsset); }); }); describe('GET /records', () => { it('should call services and respond with 200 and result', async () => { - findSingleSystemRecordServiceSpy.mockResolvedValue(fakeSystemRecord); + findSingleSystemRecordServiceSpy.mockResolvedValue(fakeAsset); findRecordServiceSpy.mockResolvedValue(fakeResult); const res = await request(app).get('/records').query({ record_id: 'rec2', system_id: 'sys2' }).expect(200); expect(findSingleSystemRecordServiceSpy).toHaveBeenCalledWith('rec2', 'sys2'); - expect(findRecordServiceSpy).toHaveBeenCalledWith(fakeSystemRecord); + expect(findRecordServiceSpy).toHaveBeenCalledWith(fakeAsset); expect(res.body).toEqual(fakeResult); }); }); diff --git a/tests/unit/controllers/systemRecord.test.ts b/tests/unit/controllers/systemRecord.test.ts index 5676af9..f7397c8 100644 --- a/tests/unit/controllers/systemRecord.test.ts +++ b/tests/unit/controllers/systemRecord.test.ts @@ -5,13 +5,13 @@ import { deleteSystemRecordController } from '#src/controllers/systemRecord'; import * as services from '#src/services/index'; import type { Selectable } from 'kysely'; -import type { PiesSystemRecord } from '#types'; +import type { PiesAsset } from '#types'; describe('System Record Controllers', () => { - const deleteSystemRecordServiceSpy = vi.spyOn(services, 'deleteSystemRecordService'); - const findSingleSystemRecordServiceSpy = vi.spyOn(services, 'findSingleSystemRecordService'); + const deleteAssetServiceSpy = vi.spyOn(services, 'deleteAssetService'); + const findSingleAssetServiceSpy = vi.spyOn(services, 'findSingleAssetService'); - const fakeSystemRecord = { id: 1 } as Selectable; + const fakeAsset = { id: 1 } as Selectable; let app: express.Application; @@ -23,13 +23,13 @@ describe('System Record Controllers', () => { describe('DELETE /system-records', () => { it('should call services and respond with 204', async () => { - findSingleSystemRecordServiceSpy.mockResolvedValue(fakeSystemRecord); - deleteSystemRecordServiceSpy.mockResolvedValue(undefined); + findSingleAssetServiceSpy.mockResolvedValue(fakeAsset); + deleteAssetServiceSpy.mockResolvedValue(undefined); await request(app).delete('/system-records').query({ record_id: 'rec1', system_id: 'sys1' }).expect(204); - expect(findSingleSystemRecordServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); - expect(deleteSystemRecordServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); + expect(findSingleAssetServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); + expect(deleteAssetServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); }); }); }); diff --git a/tests/unit/repositories/onHoldEvent.test.ts b/tests/unit/repositories/onHoldEvent.test.ts index 988648b..8b1655c 100644 --- a/tests/unit/repositories/onHoldEvent.test.ts +++ b/tests/unit/repositories/onHoldEvent.test.ts @@ -39,7 +39,7 @@ describe('OnHoldEventRepository', () => { expect(getDefinedOperations(compiled.query)).toEqual(['kind', 'from', 'where']); expect(compiled.query.kind).toBe('DeleteQueryNode'); - expect(compiled.sql).toBe('delete from "pies"."on_hold_event" where "system_record_id" = $1'); + expect(compiled.sql).toBe('delete from "pies"."on_hold_event" where "asset_id" = $1'); expect(compiled.parameters).toEqual([systemRecordId]); }); }); diff --git a/tests/unit/repositories/processEvent.test.ts b/tests/unit/repositories/processEvent.test.ts index 6b92133..c91e711 100644 --- a/tests/unit/repositories/processEvent.test.ts +++ b/tests/unit/repositories/processEvent.test.ts @@ -39,7 +39,7 @@ describe('ProcessEventRepository', () => { expect(getDefinedOperations(compiled.query)).toEqual(['kind', 'from', 'where']); expect(compiled.query.kind).toBe('DeleteQueryNode'); - expect(compiled.sql).toBe('delete from "pies"."process_event" where "system_record_id" = $1'); + expect(compiled.sql).toBe('delete from "pies"."process_event" where "asset_id" = $1'); expect(compiled.parameters).toEqual([systemRecordId]); }); }); diff --git a/tests/unit/repositories/systemRecord.test.ts b/tests/unit/repositories/systemRecord.test.ts index 1061b84..7cacd60 100644 --- a/tests/unit/repositories/systemRecord.test.ts +++ b/tests/unit/repositories/systemRecord.test.ts @@ -23,16 +23,12 @@ describe('SystemRecordRepository', () => { it('should call super with correct arguments', () => { new SystemRecordRepository(); expect(BaseRepositoryMock).toHaveBeenCalledTimes(1); - expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.systemRecord', undefined, [ - 'system_record_system_id_record_id_unique' - ]); + expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.asset', undefined, ['asset_system_id_record_id_unique']); }); it('should call super with db argument if provided', () => { new SystemRecordRepository(mockDb); - expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.systemRecord', mockDb, [ - 'system_record_system_id_record_id_unique' - ]); + expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.asset', mockDb, ['asset_system_id_record_id_unique']); }); }); }); diff --git a/tests/unit/services/record.test.ts b/tests/unit/services/record.test.ts index 7598c7b..4ee80f0 100644 --- a/tests/unit/services/record.test.ts +++ b/tests/unit/services/record.test.ts @@ -23,15 +23,15 @@ import { findRecordService, mergeRecordService, pruneRecordService, replaceRecor import type { Selectable } from 'kysely'; import type { Mock } from 'vitest'; -import type { PiesSystemRecord, Record } from '#types'; +import type { PiesAsset, Record } from '#types'; describe('recordService', () => { - const systemRecord = { + const asset = { id: 1, recordKindId: 2, systemId: 'sys-1', recordId: 'rec-1' - } as Selectable; + } as Selectable; describe('findRecordService', () => { const processEventsRaw = [ @@ -72,17 +72,17 @@ describe('recordService', () => { it('should return a Record when events are found', async () => { executeMock.execute.mockResolvedValue(processEventsRaw); - const result = await findRecordService(systemRecord); + const result = await findRecordService(asset); expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(cacheableRead).toHaveBeenNthCalledWith(1, new RecordKindRepository(), systemRecord.recordKindId); + expect(cacheableRead).toHaveBeenNthCalledWith(1, new RecordKindRepository(), asset.recordKindId); expect(cacheableRead).toHaveBeenNthCalledWith(2, new CodingRepository(), processEventsRaw[0]?.codingId); expect(ProcessEventRepository).toHaveBeenCalledTimes(1); expect(ProcessEventRepository).toHaveBeenCalledWith(expect.anything()); expect(result).toMatchObject({ kind: 'Record', - system_id: systemRecord.systemId, - record_id: systemRecord.recordId, + system_id: asset.systemId, + record_id: asset.recordId, record_kind: 'Permit', version: 'v1', on_hold_event_set: [ @@ -127,7 +127,7 @@ describe('recordService', () => { it('should throw Problem 404 if no record kind found', async () => { (cacheableRead as Mock).mockRejectedValueOnce(new Error('not found')); - await expect(findRecordService(systemRecord)).rejects.toMatchObject({ + await expect(findRecordService(asset)).rejects.toMatchObject({ status: 404, detail: 'No record kind found.' }); @@ -150,7 +150,7 @@ describe('recordService', () => { return Promise.reject(new Error('not found')); }); - await expect(() => findRecordService(systemRecord)).rejects.toMatchObject({ + await expect(() => findRecordService(asset)).rejects.toMatchObject({ status: 404, detail: 'No valid on hold codings found.' }); @@ -182,7 +182,7 @@ describe('recordService', () => { return Promise.reject(new Error('not found')); }); - await expect(() => findRecordService(systemRecord)).rejects.toMatchObject({ + await expect(() => findRecordService(asset)).rejects.toMatchObject({ status: 404, detail: 'No valid process codings found.' }); @@ -206,7 +206,7 @@ describe('recordService', () => { return { prune: pruneMock }; }); - const result = await pruneRecordService(systemRecord); + const result = await pruneRecordService(asset); expect(result).toEqual([[], []]); expect(transactionWrapper).toHaveBeenCalledTimes(1); @@ -214,7 +214,7 @@ describe('recordService', () => { expect(OnHoldEventRepository).toHaveBeenCalledWith(expect.anything()); expect(ProcessEventRepository).toHaveBeenCalledTimes(1); expect(ProcessEventRepository).toHaveBeenCalledWith(expect.anything()); - expect(pruneMock).toHaveBeenCalledWith(systemRecord.id); + expect(pruneMock).toHaveBeenCalledWith(asset.id); expect(executeMock.execute).toHaveBeenCalledTimes(2); }); }); @@ -333,11 +333,11 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); expect(createManyMock).toHaveBeenCalledWith([ { codingId: 1, - systemRecordId: 1, + assetId: 1, transactionId: recordData.transaction_id, startDate: '2024-01-01', startTime: '00:00:00', @@ -373,8 +373,8 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); - expect(deleteExceptMock).toHaveBeenCalledWith([], { systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); + expect(deleteExceptMock).toHaveBeenCalledWith([], { assetId: 1 }); }); it('should preserve matched on hold events', async () => { @@ -403,7 +403,7 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); expect(deleteExceptMock).toHaveBeenCalledTimes(0); }); @@ -420,14 +420,14 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); expect(createManyMock).toHaveBeenCalledWith([ { codingId: 1, status: 'active', statusCode: 'A', statusDescription: 'Active', - systemRecordId: 1, + assetId: 1, transactionId: recordData.transaction_id, startDate: '2024-01-01', startTime: '00:00:00', @@ -466,8 +466,8 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); - expect(deleteExceptMock).toHaveBeenCalledWith([], { systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); + expect(deleteExceptMock).toHaveBeenCalledWith([], { assetId: 1 }); }); it('should preserve matched process events', async () => { @@ -499,7 +499,7 @@ describe('recordService', () => { await replaceRecordService(recordData); - expect(findWhereMock).toHaveBeenCalledWith({ systemRecordId: 1 }); + expect(findWhereMock).toHaveBeenCalledWith({ assetId: 1 }); expect(deleteExceptMock).toHaveBeenCalledTimes(0); }); }); diff --git a/tests/unit/services/systemRecord.test.ts b/tests/unit/services/systemRecord.test.ts index fc8f880..28b8989 100644 --- a/tests/unit/services/systemRecord.test.ts +++ b/tests/unit/services/systemRecord.test.ts @@ -2,7 +2,7 @@ import { baseRepositoryMock, executeMock } from './repository.helper.ts'; import { transactionWrapper } from '#src/services/helpers/index'; -import { SystemRecordRepository } from '#src/repositories/index'; +import { AssetRepository } from '#src/repositories/index'; import { deleteSystemRecordService, findSingleSystemRecordService } from '#src/services/systemRecord'; describe('deleteSystemRecordService', () => { @@ -14,8 +14,8 @@ describe('deleteSystemRecordService', () => { expect(result).toBeUndefined(); expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId }); expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); }); @@ -25,8 +25,8 @@ describe('deleteSystemRecordService', () => { expect(result).toBeUndefined(); expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId, systemId }); expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); }); @@ -44,8 +44,8 @@ describe('findSingleSystemRecordService', () => { expect(result).toEqual(mockRecord); expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId }); expect(executeMock.execute).toHaveBeenCalledTimes(1); }); @@ -57,8 +57,8 @@ describe('findSingleSystemRecordService', () => { expect(result).toEqual(mockRecord); expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId, systemId }); expect(executeMock.execute).toHaveBeenCalledTimes(1); }); @@ -68,7 +68,7 @@ describe('findSingleSystemRecordService', () => { await expect(findSingleSystemRecordService(recordId)).rejects.toMatchObject({ status: 404, - detail: 'System record not found', + detail: 'Asset not found', record_id: recordId }); }); @@ -78,7 +78,7 @@ describe('findSingleSystemRecordService', () => { await expect(findSingleSystemRecordService(recordId, systemId)).rejects.toMatchObject({ status: 404, - detail: 'System record not found', + detail: 'Asset not found', record_id: recordId, system_id: systemId }); @@ -89,7 +89,7 @@ describe('findSingleSystemRecordService', () => { await expect(findSingleSystemRecordService(recordId)).rejects.toMatchObject({ status: 409, - detail: 'Cannot disambiguate intended system record without `system_id`', + detail: 'Cannot disambiguate intended asset without `system_id`', record_id: recordId }); });