const assert = require('node:assert/strict'); const test = require('node:test'); const db = require('../src/db/models'); const GenericDBApi = require('../src/db/api/base.api'); const { createEntityService } = require('../src/factories/service.factory'); test('GenericDBApi.update uses object signature and forwards update context', async () => { const calls = {}; const transaction = { id: 'tx-db-api' }; const currentUser = { id: 'user-1' }; const record = { async update(payload, options) { calls.update = { payload, options }; }, async setTags(value, options) { calls.setTags = { value, options }; }, }; class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', async findByPk(id, options) { calls.findByPk = { id, options }; return record; }, }; } static get ASSOCIATIONS() { return [{ field: 'tags', setter: 'setTags', isArray: true }]; } } const result = await TestDBApi.update({ id: 'record-1', data: { name: 'Updated', tags: ['a', 'b'], skipped: undefined }, currentUser, transaction, }); assert.equal(result, record); assert.deepEqual(calls.findByPk, { id: 'record-1', options: { transaction }, }); assert.deepEqual(calls.update, { payload: { updatedById: 'user-1', name: 'Updated', tags: ['a', 'b'], }, options: { transaction }, }); assert.deepEqual(calls.setTags, { value: ['a', 'b'], options: { transaction }, }); }); test('GenericDBApi.update rejects positional signature', async () => { class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', }; } } await assert.rejects( () => TestDBApi.update('record-1', { name: 'Updated' }, {}), /DBApi\.update expects an options object/, ); }); test('GenericDBApi.create uses object signature and forwards create context', async () => { const calls = {}; const transaction = { id: 'tx-create' }; const currentUser = { id: 'user-1' }; const record = { id: 'record-1', async setTags(value, options) { calls.setTags = { value, options }; }, }; class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', async create(payload, options) { calls.create = { payload, options }; return record; }, }; } static get ASSOCIATIONS() { return [{ field: 'tags', setter: 'setTags', isArray: true }]; } } const result = await TestDBApi.create({ data: { name: 'Created', tags: ['a', 'b'] }, currentUser, transaction, }); assert.equal(result, record); assert.deepEqual(calls.create, { payload: { name: 'Created', tags: ['a', 'b'], importHash: null, createdById: 'user-1', updatedById: 'user-1', }, options: { transaction }, }); assert.deepEqual(calls.setTags, { value: ['a', 'b'], options: { transaction }, }); }); test('GenericDBApi.create rejects positional signature', async () => { class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', }; } } await assert.rejects( () => TestDBApi.create({ name: 'Created' }, {}), /DBApi\.create requires \{ data \}/, ); }); test('GenericDBApi.partialUpdate uses object signature and only updates defined fields', async () => { const calls = {}; const transaction = { id: 'tx-partial' }; const currentUser = { id: 'user-1' }; const record = { async update(payload, options) { calls.update = { payload, options }; }, }; class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', async findByPk(id, options) { calls.findByPk = { id, options }; return record; }, }; } } const result = await TestDBApi.partialUpdate({ id: 'record-1', data: { name: 'Updated', skipped: undefined }, currentUser, transaction, }); assert.equal(result, record); assert.deepEqual(calls.findByPk, { id: 'record-1', options: { transaction }, }); assert.deepEqual(calls.update, { payload: { updatedById: 'user-1', name: 'Updated', }, options: { transaction }, }); }); test('GenericDBApi.partialUpdate rejects positional signature', async () => { class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', }; } } await assert.rejects( () => TestDBApi.partialUpdate('record-1', { name: 'Updated' }, {}), /DBApi\.update expects an options object/, ); }); test('createEntityService update uses object signature and manages own transaction', async () => { const originalTransaction = db.sequelize.transaction; const calls = {}; const transaction = { async commit() { calls.committed = true; }, async rollback() { calls.rolledBack = true; }, }; db.sequelize.transaction = async () => transaction; const DBApi = { async findBy(where, options) { calls.findBy = { where, options }; return { id: where.id }; }, async update(options) { calls.update = options; return { id: options.id, ...options.data }; }, }; try { const Service = createEntityService(DBApi, { entityName: 'TestEntity' }); const currentUser = { id: 'user-1' }; const runtimeContext = { environment: 'dev' }; const result = await Service.update({ id: 'record-1', data: { name: 'Updated' }, currentUser, runtimeContext, }); assert.deepEqual(result, { id: 'record-1', name: 'Updated' }); assert.deepEqual(calls.findBy, { where: { id: 'record-1' }, options: { transaction, runtimeContext }, }); assert.deepEqual(calls.update, { id: 'record-1', data: { name: 'Updated' }, currentUser, transaction, runtimeContext, }); assert.equal(calls.committed, true); assert.equal(calls.rolledBack, undefined); } finally { db.sequelize.transaction = originalTransaction; } }); test('createEntityService update rejects positional signature', async () => { const Service = createEntityService( { async findBy() { throw new Error('should not be called'); }, async update() { throw new Error('should not be called'); }, }, { entityName: 'TestEntity' }, ); await assert.rejects( () => Service.update({ name: 'Updated' }, 'record-1', { id: 'user-1' }), /Service\.update requires \{ id, data \}/, ); }); test('createEntityService create, deleteByIds, and remove use object signatures', async () => { const originalTransaction = db.sequelize.transaction; const calls = {}; const transaction = { async commit() { calls.commits = (calls.commits || 0) + 1; }, async rollback() { calls.rollbacks = (calls.rollbacks || 0) + 1; }, }; db.sequelize.transaction = async () => transaction; const DBApi = { async create(options) { calls.create = options; return { id: 'created-1', ...options.data }; }, async deleteByIds(options) { calls.deleteByIds = options; }, async remove(options) { calls.remove = options; }, }; try { const Service = createEntityService(DBApi, { entityName: 'TestEntity' }); const currentUser = { id: 'user-1' }; const runtimeContext = { environment: 'stage' }; const created = await Service.create({ data: { name: 'Created' }, currentUser, runtimeContext, }); await Service.deleteByIds({ ids: ['record-1', 'record-2'], currentUser, runtimeContext, }); await Service.remove({ id: 'record-1', currentUser, runtimeContext, }); assert.deepEqual(created, { id: 'created-1', name: 'Created' }); assert.deepEqual(calls.create, { data: { name: 'Created' }, currentUser, transaction, runtimeContext, }); assert.deepEqual(calls.deleteByIds, { ids: ['record-1', 'record-2'], currentUser, transaction, runtimeContext, }); assert.deepEqual(calls.remove, { id: 'record-1', currentUser, transaction, runtimeContext, }); assert.equal(calls.commits, 3); assert.equal(calls.rollbacks, undefined); } finally { db.sequelize.transaction = originalTransaction; } }); test('createEntityService create, deleteByIds, and remove reject positional signatures', async () => { const Service = createEntityService( { async create() { throw new Error('should not be called'); }, async deleteByIds() { throw new Error('should not be called'); }, async remove() { throw new Error('should not be called'); }, }, { entityName: 'TestEntity' }, ); await assert.rejects( () => Service.create({ name: 'Created' }, { id: 'user-1' }), /Service\.create requires \{ data \}/, ); await assert.rejects( () => Service.deleteByIds(['record-1'], { id: 'user-1' }), /Service\.deleteByIds expects an options object/, ); await assert.rejects( () => Service.remove('record-1', { id: 'user-1' }), /Service\.remove expects an options object/, ); }); test('GenericDBApi deleteByIds, remove, and findAllAutocomplete use object signatures', async () => { const calls = {}; const transaction = { id: 'tx-db-api' }; const currentUser = { id: 'user-1' }; const deletedRecords = [ { id: 'record-1', async update(payload, options) { calls.deletedUpdate = { payload, options }; }, async destroy(options) { calls.deletedDestroy = options; }, }, ]; const removedRecord = { id: 'record-2', async update(payload, options) { calls.removedUpdate = { payload, options }; }, async destroy(options) { calls.removedDestroy = options; }, }; const autocompleteRecords = [{ id: 'record-3', name: 'Alpha' }]; class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', async findAll(options) { calls.findAll = options; return options.attributes ? autocompleteRecords : deletedRecords; }, async findByPk(id, options) { calls.findByPk = { id, options }; return removedRecord; }, }; } } const deleted = await TestDBApi.deleteByIds({ ids: ['record-1'], currentUser, transaction, }); const removed = await TestDBApi.remove({ id: 'record-2', currentUser, transaction, }); const autocomplete = await TestDBApi.findAllAutocomplete( { query: 'Alpha', limit: 5, offset: 0 }, { transaction }, ); assert.equal(deleted, deletedRecords); assert.equal(removed, removedRecord); assert.deepEqual(calls.deletedUpdate, { payload: { deletedBy: 'user-1' }, options: { transaction }, }); assert.deepEqual(calls.deletedDestroy, { transaction }); assert.deepEqual(calls.findByPk, { id: 'record-2', options: { transaction }, }); assert.deepEqual(calls.removedUpdate, { payload: { deletedBy: 'user-1' }, options: { transaction }, }); assert.deepEqual(calls.removedDestroy, { transaction }); assert.deepEqual(autocomplete, [{ id: 'record-3', label: 'Alpha' }]); assert.equal(calls.findAll.limit, 5); assert.equal(calls.findAll.transaction, transaction); }); test('GenericDBApi deleteByIds, remove, and findAllAutocomplete reject positional signatures', async () => { class TestDBApi extends GenericDBApi { static get MODEL() { return { rawAttributes: {}, getTableName: () => 'test_records', }; } } await assert.rejects( () => TestDBApi.deleteByIds(['record-1'], {}), /DBApi\.deleteByIds expects an options object/, ); await assert.rejects( () => TestDBApi.remove('record-1', {}), /DBApi\.remove expects an options object/, ); await assert.rejects( () => TestDBApi.findAllAutocomplete('Alpha', 5, 0), /DBApi\.findAllAutocomplete expects an options object/, ); });