diff --git a/src/controllers/asset.ts b/src/controllers/asset.ts new file mode 100644 index 0000000..ed52522 --- /dev/null +++ b/src/controllers/asset.ts @@ -0,0 +1,13 @@ +import { deleteAssetService, findSingleAssetService } from '#src/services/index'; + +import type { Request, Response } from 'express'; +import type { AssetQuery } 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(); +}; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 453ad4f..a366ef9 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ +export * from './asset.ts'; export * from './record.ts'; -export * from './systemRecord.ts'; export * from './validate.ts'; diff --git a/src/controllers/record.ts b/src/controllers/record.ts index c684c38..367a643 100644 --- a/src/controllers/record.ts +++ b/src/controllers/record.ts @@ -1,20 +1,20 @@ import { checkDuplicateTransactionHeaderService, findRecordService, - findSingleSystemRecordService, + findSingleAssetService, pruneRecordService, replaceRecordService } from '#src/services/index'; import type { Request, Response } from 'express'; -import type { LocalContext, Record, SystemRecordQuery } from '#types'; +import type { AssetQuery, LocalContext, Record } from '#types'; export const getRecordController = async ( - req: Request, + req: Request, res: Response ): Promise => { - const systemRecord = await findSingleSystemRecordService(req.query.record_id, req.query.system_id); - const result = await findRecordService(systemRecord); + const asset = await findSingleAssetService(req.query.record_id, req.query.system_id); + const result = await findRecordService(asset); res.status(200).json(result); }; @@ -28,11 +28,11 @@ export const postRecordController = async ( }; export const pruneRecordController = async ( - req: Request, + req: Request, res: Response ): Promise => { - const systemRecord = await findSingleSystemRecordService(req.query.record_id, req.query.system_id); - await pruneRecordService(systemRecord); + const asset = await findSingleAssetService(req.query.record_id, req.query.system_id); + await pruneRecordService(asset); res.status(204).end(); }; diff --git a/src/controllers/systemRecord.ts b/src/controllers/systemRecord.ts deleted file mode 100644 index 0164620..0000000 --- a/src/controllers/systemRecord.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { deleteSystemRecordService, findSingleSystemRecordService } from '#src/services/index'; - -import type { Request, Response } from 'express'; -import type { SystemRecordQuery } from '#types'; - -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); - res.status(204).end(); -}; diff --git a/src/db/migrations/1781729760312_rename_system_record_to_asset.ts b/src/db/migrations/1781729760312_rename_system_record_to_asset.ts new file mode 100644 index 0000000..d7292c8 --- /dev/null +++ b/src/db/migrations/1781729760312_rename_system_record_to_asset.ts @@ -0,0 +1,104 @@ +/* eslint-disable max-len */ +import { sql } from 'kysely'; + +import type { Kysely } from 'kysely'; + +/** + * Renames the `pies.system_record` table (and its dependent columns, constraints, indexes and + * triggers) to `pies.asset`. + * @param db - Database + */ +export async function up(db: Kysely): Promise { + // pies.system_record -> pies.asset + await db.schema.alterTable('pies.system_record').renameTo('asset').execute(); + await sql`ALTER TABLE ${sql.id('pies', 'asset')} RENAME CONSTRAINT ${sql.id('system_record_system_id_record_id_unique')} TO ${sql.id('asset_system_id_record_id_unique')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'system_record_record_id_index')} RENAME TO ${sql.id('asset_record_id_index')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'system_record_system_id_index')} RENAME TO ${sql.id('asset_system_id_index')}`.execute( + db + ); + await sql`ALTER TRIGGER ${sql.id('pies_system_record_bu_trigger')} ON ${sql.id('pies', 'asset')} RENAME TO ${sql.id('pies_asset_bu_trigger')}`.execute( + db + ); + await sql`ALTER TRIGGER ${sql.id('audit_system_record_au_trigger')} ON ${sql.id('pies', 'asset')} RENAME TO ${sql.id('audit_asset_au_trigger')}`.execute( + db + ); + + // pies.on_hold_event.system_record_id -> asset_id + await db.schema.alterTable('pies.on_hold_event').renameColumn('system_record_id', 'asset_id').execute(); + await sql`ALTER INDEX ${sql.id('pies', 'on_hold_event_system_record_id_index')} RENAME TO ${sql.id('on_hold_event_asset_id_index')}`.execute( + db + ); + + // pies.process_event.system_record_id -> asset_id + await db.schema.alterTable('pies.process_event').renameColumn('system_record_id', 'asset_id').execute(); + await sql`ALTER INDEX ${sql.id('pies', 'process_event_system_record_id_index')} RENAME TO ${sql.id('process_event_asset_id_index')}`.execute( + db + ); + + // pies.record_linkage.system_record_id -> asset_id, linked_system_record_id -> linked_asset_id + await db.schema.alterTable('pies.record_linkage').renameColumn('system_record_id', 'asset_id').execute(); + await db.schema + .alterTable('pies.record_linkage') + .renameColumn('linked_system_record_id', 'linked_asset_id') + .execute(); + await sql`ALTER INDEX ${sql.id('pies', 'record_linkage_system_record_id_index')} RENAME TO ${sql.id('record_linkage_asset_id_index')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'record_linkage_linked_system_record_id_index')} RENAME TO ${sql.id('record_linkage_linked_asset_id_index')}`.execute( + db + ); +} + +/** + * Reverts the `pies.asset` rename back to `pies.system_record`, restoring every dependent column, + * constraint, index and trigger name. + * @param db - Database + */ +export async function down(db: Kysely): Promise { + // pies.record_linkage.asset_id -> system_record_id, linked_asset_id -> linked_system_record_id + await sql`ALTER INDEX ${sql.id('pies', 'record_linkage_linked_asset_id_index')} RENAME TO ${sql.id('record_linkage_linked_system_record_id_index')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'record_linkage_asset_id_index')} RENAME TO ${sql.id('record_linkage_system_record_id_index')}`.execute( + db + ); + await db.schema + .alterTable('pies.record_linkage') + .renameColumn('linked_asset_id', 'linked_system_record_id') + .execute(); + await db.schema.alterTable('pies.record_linkage').renameColumn('asset_id', 'system_record_id').execute(); + + // pies.process_event.asset_id -> system_record_id + await sql`ALTER INDEX ${sql.id('pies', 'process_event_asset_id_index')} RENAME TO ${sql.id('process_event_system_record_id_index')}`.execute( + db + ); + await db.schema.alterTable('pies.process_event').renameColumn('asset_id', 'system_record_id').execute(); + + // pies.on_hold_event.asset_id -> system_record_id + await sql`ALTER INDEX ${sql.id('pies', 'on_hold_event_asset_id_index')} RENAME TO ${sql.id('on_hold_event_system_record_id_index')}`.execute( + db + ); + await db.schema.alterTable('pies.on_hold_event').renameColumn('asset_id', 'system_record_id').execute(); + + // pies.asset -> pies.system_record + await sql`ALTER TRIGGER ${sql.id('audit_asset_au_trigger')} ON ${sql.id('pies', 'asset')} RENAME TO ${sql.id('audit_system_record_au_trigger')}`.execute( + db + ); + await sql`ALTER TRIGGER ${sql.id('pies_asset_bu_trigger')} ON ${sql.id('pies', 'asset')} RENAME TO ${sql.id('pies_system_record_bu_trigger')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'asset_system_id_index')} RENAME TO ${sql.id('system_record_system_id_index')}`.execute( + db + ); + await sql`ALTER INDEX ${sql.id('pies', 'asset_record_id_index')} RENAME TO ${sql.id('system_record_record_id_index')}`.execute( + db + ); + await sql`ALTER TABLE ${sql.id('pies', 'asset')} RENAME CONSTRAINT ${sql.id('asset_system_id_record_id_unique')} TO ${sql.id('system_record_system_id_record_id_unique')}`.execute( + db + ); + await db.schema.alterTable('pies.asset').renameTo('system_record').execute(); +} 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..f9e9920 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -1,3 +1,4 @@ +export * from './asset.ts'; export * from './base.ts'; export * from './coding.ts'; export * from './recordKind.ts'; @@ -5,6 +6,5 @@ export * from './onHoldEvent.ts'; export * from './processEvent.ts'; export * from './recordLinkage.ts'; export * from './system.ts'; -export * from './systemRecord.ts'; export * from './transaction.ts'; export * from './version.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 deleted file mode 100644 index 33c65ca..0000000 --- a/src/repositories/systemRecord.ts +++ /dev/null @@ -1,12 +0,0 @@ -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; - -export class SystemRecordRepository extends BaseRepository<'pies.systemRecord', (typeof CONSTRAINTS)[number]> { - constructor(db?: Kysely | Transaction) { - super('pies.systemRecord', db, CONSTRAINTS); - } -} diff --git a/src/routes/v1/system.ts b/src/routes/v1/system.ts index 966f8ab..f40598f 100644 --- a/src/routes/v1/system.ts +++ b/src/routes/v1/system.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; -import { deleteSystemRecordController } from '#src/controllers/index'; +import { deleteAssetController } from '#src/controllers/index'; import { authz } from '#src/middlewares/index'; import { Problem } from '#src/utils/index'; import { @@ -24,6 +24,6 @@ router.get('/system-records', getSystemRecordsSchemaValidator, (req: Request, re }); /** Delete Records */ -router.delete('/system-records', authz('query'), deleteSystemRecordsSchemaValidator, deleteSystemRecordController); +router.delete('/system-records', authz('query'), deleteSystemRecordsSchemaValidator, deleteAssetController); export default router; diff --git a/src/services/asset.ts b/src/services/asset.ts new file mode 100644 index 0000000..f900d68 --- /dev/null +++ b/src/services/asset.ts @@ -0,0 +1,53 @@ +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 record is found (404) or if multiple records 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 record is found (404) or if multiple records 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' } + ); +}; diff --git a/src/services/index.ts b/src/services/index.ts index 928ec23..5e354fc 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,3 @@ +export * from './asset.ts'; export * from './header.ts'; export * from './record.ts'; -export * from './systemRecord.ts'; diff --git a/src/services/record.ts b/src/services/record.ts index 8e2a42a..81876c6 100644 --- a/src/services/record.ts +++ b/src/services/record.ts @@ -9,53 +9,39 @@ import { transactionWrapper } from './helpers/index.ts'; import { + AssetRepository, CodingRepository, OnHoldEventRepository, ProcessEventRepository, RecordKindRepository, SystemRepository, - SystemRecordRepository, 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 deleted file mode 100644 index 59edb4f..0000000 --- a/src/services/systemRecord.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { transactionWrapper } from './helpers/index.ts'; -import { SystemRecordRepository } from '#src/repositories/index'; -import { Problem } from '#src/utils/index'; - -import type { Selectable, SimplifySingleResult } from 'kysely'; -import type { PiesSystemRecord } from '#types'; - -/** - * Deletes a single system record 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). - */ -export const deleteSystemRecordService = async ( - recordId: string, - systemId?: string -): Promise> => { - return transactionWrapper(async (trx) => { - return await new SystemRecordRepository(trx).deleteWhere({ recordId, systemId }).executeTakeFirst(); - }); -}; - -/** - * Finds a single system record 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). - */ -export const findSingleSystemRecordService = async ( - recordId: string, - systemId?: string -): Promise> => { - return transactionWrapper( - async (trx) => { - const systemRecords = await new SystemRecordRepository(trx).findWhere({ recordId, systemId }).execute(); - - if (!systemRecords.length) { - const params: Record = { record_id: recordId }; - if (systemId) params.system_id = systemId; - throw new Problem(404, { detail: 'System record not found' }, params); - } - if (systemRecords.length > 1) { - throw new Problem( - 409, - { detail: 'Cannot disambiguate intended system record without `system_id`' }, - { record_id: recordId } - ); - } - - return systemRecords[0]!; - }, - { accessMode: 'read only' } - ); -}; diff --git a/src/types/controllers.ts b/src/types/controllers.ts index 84e30bf..39bf0d3 100644 --- a/src/types/controllers.ts +++ b/src/types/controllers.ts @@ -13,8 +13,8 @@ export interface LocalContext { cspNonce?: string; } -/** An object that represents a typical query for a system record. */ -export interface SystemRecordQuery extends ParsedQs { +/** An object that represents a typical query for an asset. */ +export interface AssetQuery extends ParsedQs { /** The ID of the record to be queried. */ record_id: string; diff --git a/src/types/models.d.ts b/src/types/models.d.ts index 5c94214..3aabda3 100644 --- a/src/types/models.d.ts +++ b/src/types/models.d.ts @@ -10,6 +10,17 @@ export type Generated = export type Timestamp = ColumnType; +export interface PiesAsset { + createdAt: Generated; + createdBy: Generated; + id: Generated; + recordId: string; + recordKindId: number; + systemId: string; + updatedAt: Timestamp | null; + updatedBy: string | null; +} + export interface PiesCoding { code: string; codeSystem: string; @@ -22,6 +33,7 @@ export interface PiesCoding { } export interface PiesOnHoldEvent { + assetId: number; codingId: number; createdAt: Generated; createdBy: Generated; @@ -30,13 +42,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 +60,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 +76,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,17 +94,6 @@ export interface PiesSystem { updatedBy: string | null; } -export interface PiesSystemRecord { - createdAt: Generated; - createdBy: Generated; - id: Generated; - recordId: string; - recordKindId: number; - systemId: string; - updatedAt: Timestamp | null; - updatedBy: string | null; -} - export interface PiesTransaction { createdAt: Generated; createdBy: Generated; @@ -111,13 +111,13 @@ export interface PiesVersion { } export interface DB { + 'pies.asset': PiesAsset; 'pies.coding': PiesCoding; '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/asset.test.ts b/tests/unit/controllers/asset.test.ts new file mode 100644 index 0000000..64a715d --- /dev/null +++ b/tests/unit/controllers/asset.test.ts @@ -0,0 +1,35 @@ +import express from 'express'; +import request from 'supertest'; + +import { deleteAssetController } from '#src/controllers/asset'; +import * as services from '#src/services/index'; + +import type { Selectable } from 'kysely'; +import type { PiesAsset } from '#types'; + +describe('Asset Controllers', () => { + const deleteAssetServiceSpy = vi.spyOn(services, 'deleteAssetService'); + const findSingleAssetServiceSpy = vi.spyOn(services, 'findSingleAssetService'); + + const fakeAsset = { id: 1 } as Selectable; + + let app: express.Application; + + beforeEach(() => { + app = express(); + app.use(express.json()); + app.delete('/system-records', deleteAssetController); + }); + + describe('DELETE /system-records', () => { + it('should call services and respond with 204', async () => { + findSingleAssetServiceSpy.mockResolvedValue(fakeAsset); + deleteAssetServiceSpy.mockResolvedValue(undefined); + + await request(app).delete('/system-records').query({ record_id: 'rec1', system_id: 'sys1' }).expect(204); + + expect(findSingleAssetServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); + expect(deleteAssetServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); + }); + }); +}); diff --git a/tests/unit/controllers/record.test.ts b/tests/unit/controllers/record.test.ts index 5a858b9..dbba4b6 100644 --- a/tests/unit/controllers/record.test.ts +++ b/tests/unit/controllers/record.test.ts @@ -11,16 +11,16 @@ 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'); const findRecordServiceSpy = vi.spyOn(services, 'findRecordService'); - const findSingleSystemRecordServiceSpy = vi.spyOn(services, 'findSingleSystemRecordService'); + const findSingleAssetServiceSpy = vi.spyOn(services, 'findSingleAssetService'); 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); + findSingleAssetServiceSpy.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(findSingleAssetServiceSpy).toHaveBeenCalledWith('rec1', 'sys1'); + expect(pruneRecordServiceSpy).toHaveBeenCalledWith(fakeAsset); }); }); describe('GET /records', () => { it('should call services and respond with 200 and result', async () => { - findSingleSystemRecordServiceSpy.mockResolvedValue(fakeSystemRecord); + findSingleAssetServiceSpy.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(findSingleAssetServiceSpy).toHaveBeenCalledWith('rec2', 'sys2'); + 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 deleted file mode 100644 index 5676af9..0000000 --- a/tests/unit/controllers/systemRecord.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express from 'express'; -import request from 'supertest'; - -import { deleteSystemRecordController } from '#src/controllers/systemRecord'; -import * as services from '#src/services/index'; - -import type { Selectable } from 'kysely'; -import type { PiesSystemRecord } from '#types'; - -describe('System Record Controllers', () => { - const deleteSystemRecordServiceSpy = vi.spyOn(services, 'deleteSystemRecordService'); - const findSingleSystemRecordServiceSpy = vi.spyOn(services, 'findSingleSystemRecordService'); - - const fakeSystemRecord = { id: 1 } as Selectable; - - let app: express.Application; - - beforeEach(() => { - app = express(); - app.use(express.json()); - app.delete('/system-records', deleteSystemRecordController); - }); - - describe('DELETE /system-records', () => { - it('should call services and respond with 204', async () => { - findSingleSystemRecordServiceSpy.mockResolvedValue(fakeSystemRecord); - deleteSystemRecordServiceSpy.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'); - }); - }); -}); diff --git a/tests/unit/repositories/asset.test.ts b/tests/unit/repositories/asset.test.ts new file mode 100644 index 0000000..a14541d --- /dev/null +++ b/tests/unit/repositories/asset.test.ts @@ -0,0 +1,34 @@ +import { mockDb } from './repository.helper.ts'; +import { BaseRepository } from '#src/repositories/base'; +import { AssetRepository } from '#src/repositories/asset'; + +describe('AssetRepository', () => { + describe('constructor', () => { + const OriginalRepository = Object.getPrototypeOf(AssetRepository) as typeof BaseRepository; + const BaseRepositoryMock = vi.fn(); + + beforeAll(() => { + Object.setPrototypeOf(AssetRepository, BaseRepositoryMock); + }); + + afterAll(() => { + Object.setPrototypeOf(AssetRepository, OriginalRepository); + }); + + it('should extend BaseRepository', () => { + const repo = new AssetRepository(); + expect(repo).toBeInstanceOf(BaseRepository); + }); + + it('should call super with correct arguments', () => { + new AssetRepository(); + expect(BaseRepositoryMock).toHaveBeenCalledTimes(1); + expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.asset', undefined, ['asset_system_id_record_id_unique']); + }); + + it('should call super with db argument if provided', () => { + new AssetRepository(mockDb); + expect(BaseRepositoryMock).toHaveBeenCalledWith('pies.asset', mockDb, ['asset_system_id_record_id_unique']); + }); + }); +}); diff --git a/tests/unit/repositories/onHoldEvent.test.ts b/tests/unit/repositories/onHoldEvent.test.ts index 988648b..c6065ce 100644 --- a/tests/unit/repositories/onHoldEvent.test.ts +++ b/tests/unit/repositories/onHoldEvent.test.ts @@ -33,14 +33,14 @@ describe('OnHoldEventRepository', () => { }); describe('prune', () => { - it('should build a delete query for the given systemRecordId', () => { - const systemRecordId = 42; - const compiled = new OnHoldEventRepository(mockDb).prune(systemRecordId).compile(); + it('should build a delete query for the given assetId', () => { + const assetId = 42; + const compiled = new OnHoldEventRepository(mockDb).prune(assetId).compile(); 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.parameters).toEqual([systemRecordId]); + expect(compiled.sql).toBe('delete from "pies"."on_hold_event" where "asset_id" = $1'); + expect(compiled.parameters).toEqual([assetId]); }); }); }); diff --git a/tests/unit/repositories/processEvent.test.ts b/tests/unit/repositories/processEvent.test.ts index 6b92133..f68efce 100644 --- a/tests/unit/repositories/processEvent.test.ts +++ b/tests/unit/repositories/processEvent.test.ts @@ -33,14 +33,14 @@ describe('ProcessEventRepository', () => { }); describe('prune', () => { - it('should build a delete query for the given systemRecordId', () => { - const systemRecordId = 42; - const compiled = new ProcessEventRepository(mockDb).prune(systemRecordId).compile(); + it('should build a delete query for the given assetId', () => { + const assetId = 42; + const compiled = new ProcessEventRepository(mockDb).prune(assetId).compile(); 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.parameters).toEqual([systemRecordId]); + expect(compiled.sql).toBe('delete from "pies"."process_event" where "asset_id" = $1'); + expect(compiled.parameters).toEqual([assetId]); }); }); }); diff --git a/tests/unit/repositories/systemRecord.test.ts b/tests/unit/repositories/systemRecord.test.ts deleted file mode 100644 index 1061b84..0000000 --- a/tests/unit/repositories/systemRecord.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { mockDb } from './repository.helper.ts'; -import { BaseRepository } from '#src/repositories/base'; -import { SystemRecordRepository } from '#src/repositories/systemRecord'; - -describe('SystemRecordRepository', () => { - describe('constructor', () => { - const OriginalRepository = Object.getPrototypeOf(SystemRecordRepository) as typeof BaseRepository; - const BaseRepositoryMock = vi.fn(); - - beforeAll(() => { - Object.setPrototypeOf(SystemRecordRepository, BaseRepositoryMock); - }); - - afterAll(() => { - Object.setPrototypeOf(SystemRecordRepository, OriginalRepository); - }); - - it('should extend BaseRepository', () => { - const repo = new SystemRecordRepository(); - expect(repo).toBeInstanceOf(BaseRepository); - }); - - 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' - ]); - }); - - 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' - ]); - }); - }); -}); diff --git a/tests/unit/routes/v1/system.test.ts b/tests/unit/routes/v1/system.test.ts index 49a047c..dfb9396 100644 --- a/tests/unit/routes/v1/system.test.ts +++ b/tests/unit/routes/v1/system.test.ts @@ -1,7 +1,7 @@ import express from 'express'; import request from 'supertest'; -import { deleteSystemRecordController } from '#src/controllers/index'; +import { deleteAssetController } from '#src/controllers/index'; import { deleteSystemRecordsSchemaValidator, getSystemRecordsSchemaValidator, @@ -15,8 +15,8 @@ import type { RequestHandler } from 'express'; const app = express(); app.use(router); -vi.mock('#src/controllers/record', () => ({ - deleteSystemRecordController: vi.fn((_req, _res, next) => next()) +vi.mock('#src/controllers/asset', () => ({ + deleteAssetController: vi.fn((_req, _res, next) => next()) })); vi.mock('#src/middlewares/auth', () => ({ @@ -49,7 +49,7 @@ describe('System Routes', () => { it('should call the schema validator and controller', async () => { await request(app).delete('/system-records'); expect(deleteSystemRecordsSchemaValidator).toHaveBeenCalled(); - expect(deleteSystemRecordController).toHaveBeenCalled(); + expect(deleteAssetController).toHaveBeenCalled(); }); }); }); diff --git a/tests/unit/services/asset.test.ts b/tests/unit/services/asset.test.ts new file mode 100644 index 0000000..af322a8 --- /dev/null +++ b/tests/unit/services/asset.test.ts @@ -0,0 +1,96 @@ +// Always import repository.helper.ts and helpers/index.ts first to ensure mocks are set up +import { baseRepositoryMock, executeMock } from './repository.helper.ts'; +import { transactionWrapper } from '#src/services/helpers/index'; + +import { AssetRepository } from '#src/repositories/index'; +import { deleteAssetService, findSingleAssetService } from '#src/services/asset'; + +describe('deleteAssetService', () => { + const recordId = 'rec-123'; + const systemId = 'sys-456'; + + it('returns a single asset without systemId specified', async () => { + const result = await deleteAssetService(recordId); + + expect(result).toBeUndefined(); + expect(transactionWrapper).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); + expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId }); + expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); + }); + + it('returns a single asset with systemId specified', async () => { + const result = await deleteAssetService(recordId, systemId); + + expect(result).toBeUndefined(); + expect(transactionWrapper).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); + expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId, systemId }); + expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); + }); +}); + +describe('findSingleAssetService', () => { + const recordId = 'rec-123'; + const systemId = 'sys-456'; + const mockRecord = { recordId, systemId, foo: 'bar' }; + + it('returns a single asset without systemId specified', async () => { + executeMock.execute.mockResolvedValue([mockRecord]); + + const result = await findSingleAssetService(recordId); + + expect(result).toEqual(mockRecord); + expect(transactionWrapper).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); + expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId }); + expect(executeMock.execute).toHaveBeenCalledTimes(1); + }); + + it('returns a single asset with systemId specified', async () => { + executeMock.execute.mockResolvedValue([mockRecord]); + + const result = await findSingleAssetService(recordId, systemId); + + expect(result).toEqual(mockRecord); + expect(transactionWrapper).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledTimes(1); + expect(AssetRepository).toHaveBeenCalledWith(expect.anything()); + expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId, systemId }); + expect(executeMock.execute).toHaveBeenCalledTimes(1); + }); + + it('throws not found error if no asset found without systemId specified', async () => { + executeMock.execute.mockResolvedValue([]); + + await expect(findSingleAssetService(recordId)).rejects.toMatchObject({ + status: 404, + detail: 'Asset not found', + record_id: recordId + }); + }); + + it('throws not found error if no asset found with systemId specified', async () => { + executeMock.execute.mockResolvedValue([]); + + await expect(findSingleAssetService(recordId, systemId)).rejects.toMatchObject({ + status: 404, + detail: 'Asset not found', + record_id: recordId, + system_id: systemId + }); + }); + + it('throws conflict error if multiple records found without systemId specified', async () => { + executeMock.execute.mockResolvedValue([mockRecord, mockRecord]); + + await expect(findSingleAssetService(recordId)).rejects.toMatchObject({ + status: 409, + detail: 'Cannot disambiguate intended asset without `system_id`', + record_id: recordId + }); + }); +}); diff --git a/tests/unit/services/record.test.ts b/tests/unit/services/record.test.ts index 7598c7b..190dbaa 100644 --- a/tests/unit/services/record.test.ts +++ b/tests/unit/services/record.test.ts @@ -10,12 +10,12 @@ import { } from '#src/services/helpers/index'; import { + AssetRepository, CodingRepository, OnHoldEventRepository, ProcessEventRepository, RecordKindRepository, SystemRepository, - SystemRecordRepository, TransactionRepository, VersionRepository } from '#src/repositories/index'; @@ -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); }); }); @@ -292,7 +292,7 @@ describe('recordService', () => { (RecordKindRepository as Mock).mockImplementation(function () { return { upsert: cacheableUpsert }; }); - (SystemRecordRepository as Mock).mockImplementation(function () { + (AssetRepository as Mock).mockImplementation(function () { return { upsert: cacheableUpsert }; }); (OnHoldEventRepository as Mock).mockImplementation(function () { @@ -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 deleted file mode 100644 index fc8f880..0000000 --- a/tests/unit/services/systemRecord.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Always import repository.helper.ts and helpers/index.ts first to ensure mocks are set up -import { baseRepositoryMock, executeMock } from './repository.helper.ts'; -import { transactionWrapper } from '#src/services/helpers/index'; - -import { SystemRecordRepository } from '#src/repositories/index'; -import { deleteSystemRecordService, findSingleSystemRecordService } from '#src/services/systemRecord'; - -describe('deleteSystemRecordService', () => { - const recordId = 'rec-123'; - const systemId = 'sys-456'; - - it('returns a single system record without systemId specified', async () => { - const result = await deleteSystemRecordService(recordId); - - expect(result).toBeUndefined(); - expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); - expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId }); - expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); - }); - - it('returns a single system record with systemId specified', async () => { - const result = await deleteSystemRecordService(recordId, systemId); - - expect(result).toBeUndefined(); - expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); - expect(baseRepositoryMock.deleteWhere).toHaveBeenCalledWith({ recordId, systemId }); - expect(executeMock.executeTakeFirst).toHaveBeenCalledTimes(1); - }); -}); - -describe('findSingleSystemRecordService', () => { - const recordId = 'rec-123'; - const systemId = 'sys-456'; - const mockRecord = { recordId, systemId, foo: 'bar' }; - - it('returns a single system record without systemId specified', async () => { - executeMock.execute.mockResolvedValue([mockRecord]); - - const result = await findSingleSystemRecordService(recordId); - - expect(result).toEqual(mockRecord); - expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); - expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId }); - expect(executeMock.execute).toHaveBeenCalledTimes(1); - }); - - it('returns a single system record with systemId specified', async () => { - executeMock.execute.mockResolvedValue([mockRecord]); - - const result = await findSingleSystemRecordService(recordId, systemId); - - expect(result).toEqual(mockRecord); - expect(transactionWrapper).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledTimes(1); - expect(SystemRecordRepository).toHaveBeenCalledWith(expect.anything()); - expect(baseRepositoryMock.findWhere).toHaveBeenCalledWith({ recordId, systemId }); - expect(executeMock.execute).toHaveBeenCalledTimes(1); - }); - - it('throws not found error if no system record found without systemId specified', async () => { - executeMock.execute.mockResolvedValue([]); - - await expect(findSingleSystemRecordService(recordId)).rejects.toMatchObject({ - status: 404, - detail: 'System record not found', - record_id: recordId - }); - }); - - it('throws not found error if no system record found with systemId specified', async () => { - executeMock.execute.mockResolvedValue([]); - - await expect(findSingleSystemRecordService(recordId, systemId)).rejects.toMatchObject({ - status: 404, - detail: 'System record not found', - record_id: recordId, - system_id: systemId - }); - }); - - it('throws conflict error if multiple records found without systemId specified', async () => { - executeMock.execute.mockResolvedValue([mockRecord, mockRecord]); - - await expect(findSingleSystemRecordService(recordId)).rejects.toMatchObject({ - status: 409, - detail: 'Cannot disambiguate intended system record without `system_id`', - record_id: recordId - }); - }); -});