487 lines
12 KiB
JavaScript
487 lines
12 KiB
JavaScript
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/,
|
|
);
|
|
});
|