UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

979 lines (879 loc) 24.1 kB
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]); }); }); });