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..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/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/1781727710316_rename_system_record_to_asset.ts b/src/db/migrations/1781727710316_rename_system_record_to_asset.ts new file mode 100644 index 0000000..343f041 --- /dev/null +++ b/src/db/migrations/1781727710316_rename_system_record_to_asset.ts @@ -0,0 +1,46 @@ +/* eslint-disable max-len */ +import { sql } from 'kysely'; + +import type { Kysely } from 'kysely'; + +/** + * @param db - Database + */ +export async function up(db: Kysely): Promise { + // Rename the table itself + await sql`ALTER TABLE pies.system_record RENAME TO asset`.execute(db); + + // Rename the unique constraint to match new table name + await sql`ALTER TABLE pies.asset RENAME CONSTRAINT system_record_system_id_record_id_unique TO asset_system_id_record_id_unique`.execute( + db + ); + + // Rename indexes (not auto-renamed on table rename in PostgreSQL) + await sql`ALTER INDEX pies.system_record_record_id_index RENAME TO asset_record_id_index`.execute(db); + await sql`ALTER INDEX pies.system_record_system_id_index RENAME TO asset_system_id_index`.execute(db); + + // Rename triggers to match new table name + await sql`ALTER TRIGGER audit_system_record_au_trigger ON pies.asset RENAME TO audit_asset_au_trigger`.execute(db); + await sql`ALTER TRIGGER pies_system_record_bu_trigger ON pies.asset RENAME TO pies_asset_bu_trigger`.execute(db); +} + +/** + * @param db - Database + */ +export async function down(db: Kysely): Promise { + // Reverse trigger renames + await sql`ALTER TRIGGER pies_asset_bu_trigger ON pies.asset RENAME TO pies_system_record_bu_trigger`.execute(db); + await sql`ALTER TRIGGER audit_asset_au_trigger ON pies.asset RENAME TO audit_system_record_au_trigger`.execute(db); + + // Reverse index renames + await sql`ALTER INDEX pies.asset_system_id_index RENAME TO system_record_system_id_index`.execute(db); + await sql`ALTER INDEX pies.asset_record_id_index RENAME TO system_record_record_id_index`.execute(db); + + // Reverse constraint rename + await sql`ALTER TABLE pies.asset RENAME CONSTRAINT asset_system_id_record_id_unique TO system_record_system_id_record_id_unique`.execute( + db + ); + + // Rename the table back + await sql`ALTER TABLE pies.asset RENAME TO system_record`.execute(db); +} 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..1b4582c 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -5,6 +5,6 @@ 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'; 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..6c76b03 100644 --- a/src/routes/v1/system.ts +++ b/src/routes/v1/system.ts @@ -1,11 +1,11 @@ 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 { - deleteSystemRecordsSchemaValidator, - getSystemRecordsSchemaValidator, + deleteAssetsSchemaValidator, + getAssetsSchemaValidator, getSystemsSchemaValidator } from '#src/validators/index'; @@ -19,11 +19,11 @@ router.get('/systems', getSystemsSchemaValidator, (req: Request, res: Response): }); /** Get Records */ -router.get('/system-records', getSystemRecordsSchemaValidator, (req: Request, res: Response): void => { +router.get('/system-records', getAssetsSchemaValidator, (req: Request, res: Response): void => { new Problem(501).send(req, res); }); /** Delete Records */ -router.delete('/system-records', authz('query'), deleteSystemRecordsSchemaValidator, deleteSystemRecordController); +router.delete('/system-records', authz('query'), deleteAssetsSchemaValidator, deleteAssetController); export default router; diff --git a/src/services/systemRecord.ts b/src/services/asset.ts similarity index 61% rename from src/services/systemRecord.ts rename to src/services/asset.ts index 59edb4f..ab227be 100644 --- a/src/services/systemRecord.ts +++ b/src/services/asset.ts @@ -1,9 +1,9 @@ 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. @@ -12,12 +12,12 @@ import type { PiesSystemRecord } from '#types'; * @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 ( +export const deleteAssetService = 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(); }); }; @@ -28,28 +28,25 @@ export const deleteSystemRecordService = async ( * @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> => { +export const findSingleAssetService = async (recordId: string, systemId?: string): 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/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..d833deb 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({ systemRecordId: asset.id }).execute(); - const onHoldEventsRaw = await new OnHoldEventRepository(trx) - .findWhere({ systemRecordId: systemRecord.id }) - .execute(); + const onHoldEventsRaw = await new OnHoldEventRepository(trx).findWhere({ systemRecordId: 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,14 +153,14 @@ 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( + const oheDb = (await new OnHoldEventRepository(trx).findWhere({ systemRecordId: asset.id }).execute()).map( (ohe) => ({ id: ohe.id, codingId: ohe.codingId, @@ -212,7 +196,7 @@ export const replaceRecordService = (data: PiesRecord, principal?: string): Prom if (oheMatched) return oheMatched.id; return { codingId, - systemRecordId: systemRecord.id, + systemRecordId: asset.id, transactionId: data.transaction_id, ...eventToDateTimeParts(ce.event), createdBy: principal @@ -224,7 +208,7 @@ 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( + const peDb = (await new ProcessEventRepository(trx).findWhere({ systemRecordId: asset.id }).execute()).map( (pe) => ({ id: pe.id, codingId: pe.codingId, @@ -281,7 +265,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, + systemRecordId: asset.id, transactionId: data.transaction_id, ...eventToDateTimeParts(pe.event), createdBy: principal @@ -295,9 +279,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, { systemRecordId: asset.id }).execute(), peMatchedIds.length < peDb.length && - new ProcessEventRepository(trx).deleteExcept(peMatchedIds, { systemRecordId: systemRecord.id }).execute() + new ProcessEventRepository(trx).deleteExcept(peMatchedIds, { systemRecordId: asset.id }).execute() ]); await Promise.all([ oheAdd.length && new OnHoldEventRepository(trx).createMany(oheAdd).execute(), 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..8bbcc42 100644 --- a/src/types/models.d.ts +++ b/src/types/models.d.ts @@ -83,7 +83,7 @@ export interface PiesSystem { updatedBy: string | null; } -export interface PiesSystemRecord { +export interface PiesAsset { createdAt: Generated; createdBy: Generated; id: Generated; @@ -117,7 +117,7 @@ export interface DB { 'pies.recordKind': PiesRecordKind; 'pies.recordLinkage': PiesRecordLinkage; 'pies.system': PiesSystem; - 'pies.systemRecord': PiesSystemRecord; + 'pies.asset': PiesAsset; 'pies.transaction': PiesTransaction; 'pies.version': PiesVersion; } diff --git a/src/validators/system.ts b/src/validators/system.ts index 8d6d463..28e0482 100644 --- a/src/validators/system.ts +++ b/src/validators/system.ts @@ -3,7 +3,7 @@ import { validateRequestSchema } from '#src/middlewares/index'; import type { RequestHandler } from 'express'; -export const deleteSystemRecordsSchemaValidator: RequestHandler = validateRequestSchema({ +export const deleteAssetsSchemaValidator: RequestHandler = validateRequestSchema({ query: { type: 'object', properties: { record_id, system_id }, @@ -11,7 +11,7 @@ export const deleteSystemRecordsSchemaValidator: RequestHandler = validateReques } }); -export const getSystemRecordsSchemaValidator: RequestHandler = validateRequestSchema({ +export const getAssetsSchemaValidator: RequestHandler = validateRequestSchema({ query: { type: 'object', properties: { record_id, system_id }, 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/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..bfb7493 100644 --- a/tests/unit/routes/v1/system.test.ts +++ b/tests/unit/routes/v1/system.test.ts @@ -1,10 +1,10 @@ import express from 'express'; import request from 'supertest'; -import { deleteSystemRecordController } from '#src/controllers/index'; +import { deleteAssetController } from '#src/controllers/index'; import { - deleteSystemRecordsSchemaValidator, - getSystemRecordsSchemaValidator, + deleteAssetsSchemaValidator, + getAssetsSchemaValidator, getSystemsSchemaValidator } from '#src/validators/index'; @@ -16,7 +16,7 @@ const app = express(); app.use(router); vi.mock('#src/controllers/record', () => ({ - deleteSystemRecordController: vi.fn((_req, _res, next) => next()) + deleteAssetController: vi.fn((_req, _res, next) => next()) })); vi.mock('#src/middlewares/auth', () => ({ @@ -40,7 +40,7 @@ describe('System Routes', () => { describe('GET /system-records', () => { it('should return 501', async () => { const response = await request(app).get('/system-records'); - expect(getSystemRecordsSchemaValidator).toHaveBeenCalled(); + expect(getAssetsSchemaValidator).toHaveBeenCalled(); expect(response.status).toBe(501); }); }); @@ -48,8 +48,8 @@ describe('System Routes', () => { describe('DELETE /system-records', () => { it('should call the schema validator and controller', async () => { await request(app).delete('/system-records'); - expect(deleteSystemRecordsSchemaValidator).toHaveBeenCalled(); - expect(deleteSystemRecordController).toHaveBeenCalled(); + expect(deleteAssetsSchemaValidator).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..121d58a --- /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 assets 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..1467036 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 () { 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 - }); - }); -});