@getanthill/datastore
Version:
Event-Sourced Datastore
979 lines (879 loc) • 24.1 kB
text/typescript
import { Datastore } from '.';
import * as utils from './utils';
import Broker from '../services/broker';
describe('utils', () => {
let datastores: Map<string, Datastore>;
let broker: Broker;
beforeEach(() => {
datastores = new Map([
['a', new Datastore()],
['b', new Datastore()],
]);
jest.spyOn(datastores.get('a')!, 'minEventsVersion').mockResolvedValue(0);
jest.spyOn(datastores.get('b')!, 'minEventsVersion').mockResolvedValue(0);
jest.spyOn(datastores.get('a')!, 'maxEventsVersion').mockResolvedValue(0);
jest.spyOn(datastores.get('b')!, 'maxEventsVersion').mockResolvedValue(0);
broker = new Broker({}, {});
});
afterEach(() => {
jest.restoreAllMocks();
});
function mockMinEventsVersion(datastore: string, entities: any[]) {
jest
.spyOn(datastores.get(datastore)!, 'minEventsVersion')
.mockImplementation((model, query) => {
if (typeof query?.version?.$gt === 'number') {
return Math.min(
...entities
.filter((e) => e.version > query?.version?.$gt)
.map((e) => e.version ?? 0),
);
}
return Math.min(...entities.map((e) => e.version ?? 0));
});
}
function mockMaxEventsVersion(datastore: string, entities: any[]) {
jest
.spyOn(datastores.get(datastore)!, 'maxEventsVersion')
.mockResolvedValue(Math.max(...entities.map((e) => e.version ?? 0)));
}
function mockWalkNext(datastore: string, entities: any[], headers = {}) {
mockMinEventsVersion(datastore, entities);
mockMaxEventsVersion(datastore, entities);
return jest
.spyOn(datastores.get(datastore)!, 'walkNext')
.mockImplementation((_model, _query, _source, page, pageSize, opts) => {
let data = [...entities];
if (opts.version_ordered === true) {
data = data.filter((d) => d.version === opts.current_version);
}
return {
data: data.slice(page * pageSize, (page + 1) * pageSize),
headers,
};
});
}
describe('#objToJsonSchema', () => {
it('returns null JSON type', () => {
const schema = utils.objToJsonSchema(null);
expect(schema).toEqual({
type: 'null',
nullable: true,
});
const validate = broker.ajv.compile(schema);
expect(validate(null)).toEqual(true);
expect(validate(undefined)).toEqual(false);
expect(validate('invalid')).toEqual(false);
expect(validate(12)).toEqual(false);
});
it('returns string JSON type (direct)', () => {
const schema = utils.objToJsonSchema('hello');
expect(schema).toEqual({
enum: ['hello'],
type: 'string',
});
const validate = broker.ajv.compile(schema);
expect(validate('hello')).toEqual(true);
expect(validate('invalid')).toEqual(false);
expect(validate(12)).toEqual(false);
});
it('returns string JSON type', () => {
const schema = utils.objToJsonSchema({ a: 'hello' });
expect(schema).toEqual({
required: ['a'],
properties: {
a: {
enum: ['hello'],
type: 'string',
},
},
type: 'object',
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: 'hello' })).toEqual(true);
expect(validate({ a: 'invalid' })).toEqual(false);
expect(validate({ a: 12 })).toEqual(false);
});
it('returns date JSON type', () => {
const schema = utils.objToJsonSchema({
a: new Date('2021-01-01T00:00:00.000Z'),
});
expect(schema).toEqual({
required: ['a'],
properties: {
a: {
enum: ['2021-01-01T00:00:00.000Z'],
type: 'string',
},
},
type: 'object',
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: '2021-01-01T00:00:00.000Z' })).toEqual(true);
expect(validate({ a: '2022-01-01T00:00:00.000Z' })).toEqual(false);
expect(validate({ a: new Date('2021-01-01T00:00:00.000Z') })).toEqual(
false,
);
});
it('returns number JSON type', () => {
const schema = utils.objToJsonSchema({ a: 12 });
expect(schema).toEqual({
required: ['a'],
properties: {
a: {
enum: [12],
type: 'number',
},
},
type: 'object',
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: 12 })).toEqual(true);
expect(validate({ a: 13 })).toEqual(false);
expect(validate({ a: '12' })).toEqual(false);
});
it('returns boolean JSON type', () => {
const schema = utils.objToJsonSchema({ a: true });
expect(schema).toEqual({
required: ['a'],
properties: {
a: {
enum: [true],
type: 'boolean',
},
},
type: 'object',
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: true })).toEqual(true);
expect(validate({ a: false })).toEqual(false);
expect(validate({ a: 'true' })).toEqual(false);
expect(validate({ a: 1 })).toEqual(false);
});
it('returns array JSON type', () => {
const schema = utils.objToJsonSchema({ a: [true, false] });
expect(schema).toEqual({
required: ['a'],
properties: {
a: {
type: 'array',
items: [
{
type: 'boolean',
enum: [true],
},
{
type: 'boolean',
enum: [false],
},
],
},
},
type: 'object',
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: [true, false] })).toEqual(true);
// Other order
expect(validate({ a: [false, true] })).toEqual(false);
expect(validate({ a: false })).toEqual(false);
expect(validate({ a: 'true' })).toEqual(false);
expect(validate({ a: 1 })).toEqual(false);
});
it('remooves the required from schema if none is defined', () => {
const schema = utils.objToJsonSchema({});
expect(schema).toEqual({
type: 'object',
properties: {},
});
const validate = broker.ajv.compile(schema);
expect(validate({ a: [true, false] })).toEqual(true);
// Other order
expect(validate({ a: [false, true] })).toEqual(true);
expect(validate({ a: false })).toEqual(true);
expect(validate({ a: 'true' })).toEqual(true);
expect(validate({ a: 1 })).toEqual(true);
});
});
describe('#getMinVersions', () => {
it('returns an empty array if no query is provided', async () => {
mockMinEventsVersion('a', []);
const minVersions = await utils.getMinVersions(datastores, []);
expect(minVersions).toEqual([]);
});
it('returns the min version of the query', async () => {
mockMinEventsVersion('a', [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 0,
version: 1,
},
]);
const minVersions = await utils.getMinVersions(datastores, [
{
datastore: 'a',
model: 'users',
query: {},
source: 'events',
},
]);
expect(minVersions).toEqual([1]);
});
it('returns 0 if a query does not map a valid datastore', async () => {
mockMinEventsVersion('a', [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 0,
version: 1,
},
]);
const minVersions = await utils.getMinVersions(datastores, [
{
datastore: 'unknown',
model: 'users',
query: {},
source: 'events',
},
]);
expect(minVersions).toEqual([0]);
});
});
describe('#getMaxVersions', () => {
it('returns an empty array if no query is provided', async () => {
mockMaxEventsVersion('a', []);
const maxVersions = await utils.getMaxVersions(datastores, []);
expect(maxVersions).toEqual([]);
});
it('returns the max version of the query', async () => {
mockMaxEventsVersion('a', [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 0,
version: 1,
},
]);
const maxVersions = await utils.getMaxVersions(datastores, [
{
datastore: 'a',
model: 'users',
query: {},
source: 'events',
},
]);
expect(maxVersions).toEqual([1]);
});
it('returns -1 if a query does not map a valid datastore', async () => {
mockMaxEventsVersion('a', [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 0,
version: 1,
},
]);
const maxVersions = await utils.getMaxVersions(datastores, [
{
datastore: 'unknown',
model: 'users',
query: {},
source: 'events',
},
]);
expect(maxVersions).toEqual([-1]);
});
});
describe('#defaultWalkMultiSortHandler', () => {
it('returns -1 if b created later than a', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
},
{
created_at: '2020-02-01T00:00:00.000Z',
},
),
).toEqual(-1);
});
it('returns -1 if b updated later than a', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
updated_at: '2020-01-01T00:00:00.000Z',
},
{
updated_at: '2020-02-01T00:00:00.000Z',
},
),
).toEqual(-1);
});
it('returns 0 if b created is the same than a created and versions are the sames', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
),
).toEqual(0);
});
it('returns 0 if b created is the same than a created and versions are both undefined', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
},
{
created_at: '2020-01-01T00:00:00.000Z',
},
),
).toEqual(0);
});
it('returns -1 if b created is the same than a created and a version is undefined', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
},
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
),
).toEqual(-1);
});
it('returns 1 if b created is the same than a created and b version is undefined', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
{
created_at: '2020-01-01T00:00:00.000Z',
},
),
).toEqual(1);
});
it('returns -1 if b created is the same than a created and b version is greater than a version', () => {
expect(
utils.defaultWalkMultiSortHandler(
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
{
created_at: '2020-01-01T00:00:00.000Z',
version: 2,
},
),
).toEqual(-1);
});
});
describe('#sortResults', () => {
it('sorts entities by created date', () => {
expect(
utils.sortResults([
{
created_at: '2020-02-01T00:00:00.000Z',
},
{
created_at: '2020-01-01T00:00:00.000Z',
},
]),
).toEqual([
{
created_at: '2020-01-01T00:00:00.000Z',
},
{
created_at: '2020-02-01T00:00:00.000Z',
},
]);
});
it('sorts entities on the same instant by version', () => {
expect(
utils.sortResults([
{
created_at: '2020-01-01T00:00:00.000Z',
version: 2,
},
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
]),
).toEqual([
{
created_at: '2020-01-01T00:00:00.000Z',
version: 1,
},
{
created_at: '2020-01-01T00:00:00.000Z',
version: 2,
},
]);
});
it('sorts entities with a defined sort handler', () => {
expect(
utils.sortResults(
[
{
expired_at: '2020-02-01T00:00:00.000Z',
},
{
expired_at: '2020-01-01T00:00:00.000Z',
},
],
(a, b) => a.expired_at.localeCompare(b.expired_at),
),
).toEqual([
{
expired_at: '2020-01-01T00:00:00.000Z',
},
{
expired_at: '2020-02-01T00:00:00.000Z',
},
]);
});
});
describe('#walkMulti', () => {
let processedEntities: any[] = [];
let handler;
beforeEach(() => {
processedEntities = [];
handler = jest.fn().mockImplementation((entity) => {
processedEntities.push(entity);
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('returns entities from one datastore and `pageSize=1`', async () => {
const entities = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
];
const mock = mockWalkNext('a', entities);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'entities',
},
],
1,
handler,
{
sleep: 1,
},
);
expect(mock).toHaveBeenCalledWith('users', {}, 'entities', 0, 2, {
current_version: -1,
headers: undefined,
version_ordered: false,
cursor_last_id: '',
cursor_last_correlation_id: '',
});
expect(processedEntities).toEqual(entities);
});
it('returns entities from one datastore and special `_fields` set in query', async () => {
const entities = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
];
const mock = mockWalkNext('a', entities);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {
_fields: {
value: 1,
},
},
model: 'users',
source: 'entities',
},
],
1,
handler,
{
sleep: 1,
},
);
expect(mock).toHaveBeenCalledWith(
'users',
{ _fields: { created_at: 1, updated_at: 1, value: 1 } },
'entities',
0,
2,
{
current_version: -1,
headers: undefined,
version_ordered: false,
cursor_last_id: '',
cursor_last_correlation_id: '',
},
);
expect(processedEntities).toEqual(entities);
});
it('returns entities from two datastores and `pageSize=1`', async () => {
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
];
const entitiesB = [
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
},
];
const mockA = mockWalkNext('a', entitiesA);
const mockB = mockWalkNext('b', entitiesB);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'entities',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'entities',
},
],
1,
handler,
);
expect(processedEntities).toEqual([...entitiesA, ...entitiesB]);
});
it('returns entities from two datastores in correct `created_at` order', async () => {
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
},
];
const entitiesB = [
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
},
];
const mockA = mockWalkNext('a', entitiesA);
const mockB = mockWalkNext('b', entitiesB);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'entities',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'entities',
},
],
1,
handler,
);
expect(processedEntities.map((e) => e.value)).toEqual([1, 2, 3, 4, 5, 6]);
});
it('returns entities from two datastores in correct `created_at` order and `pageSize=2`', async () => {
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
},
];
const entitiesB = [
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
},
];
const mockA = mockWalkNext('a', entitiesA);
const mockB = mockWalkNext('b', entitiesB);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'entities',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'entities',
},
],
2,
handler,
);
expect(processedEntities.map((e) => e.value)).toEqual([1, 2, 3, 4, 5, 6]);
});
it('throws an error if the same correlation last id after one iteration', async () => {
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
},
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
},
];
const entitiesB = [
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
},
];
const mockA = mockWalkNext('a', entitiesA, {
'cursor-last-id': 'same-cursor',
});
const mockB = mockWalkNext('b', entitiesB);
let error;
try {
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'entities',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'entities',
},
],
1,
handler,
);
} catch (err) {
error = err;
}
expect(error.message).toEqual('Same cursor last id after iteration');
});
it('returns events from two datastores in correct `created_at` order and version ordered', async () => {
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
version: 0,
},
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
version: 1,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
version: 2,
},
];
const entitiesB = [
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
version: 0,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
version: 1,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
version: 2,
},
];
const mockA = mockWalkNext('a', entitiesA);
const mockB = mockWalkNext('b', entitiesB);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'events',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'events',
},
],
2,
handler,
);
expect(processedEntities.map((e) => e.value)).toEqual([1, 2, 3, 4, 5, 6]);
});
it('returns events from two datastores with a hole in version number for query', async () => {
jest.spyOn(datastores.get('a')!, 'maxEventsVersion').mockResolvedValue(4);
const entitiesA = [
{
created_at: '2020-01-01T00:00:00.000Z',
value: 1,
version: 0,
},
{
created_at: '2020-01-04T00:00:00.000Z',
value: 4,
version: 1,
},
{
created_at: '2020-01-06T00:00:00.000Z',
value: 6,
version: 4, // <- we skipped version 3 here
},
];
const entitiesB = [
{
created_at: '2020-01-02T00:00:00.000Z',
value: 2,
version: 0,
},
{
created_at: '2020-01-03T00:00:00.000Z',
value: 3,
version: 1,
},
{
created_at: '2020-01-05T00:00:00.000Z',
value: 5,
version: 2,
},
];
const mockA = mockWalkNext('a', entitiesA);
const mockB = mockWalkNext('b', entitiesB);
await utils.walkMulti(
datastores,
[
{
datastore: 'a',
query: {},
model: 'users',
source: 'events',
},
{
datastore: 'b',
query: {},
model: 'users',
source: 'events',
},
],
2,
handler,
);
expect(processedEntities.map((e) => e.value)).toEqual([1, 2, 3, 4, 5, 6]);
});
});
});