UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

1,551 lines (1,304 loc) 44.2 kB
import Datastore, { ERROR_MISSING_CORRELATION_ID, ERROR_MISSING_JSON_PATCH, ERROR_MISSING_MODEL_NAME, } from './Datastore'; import { EventEmitter } from 'events'; describe('sdk/Datastore', () => { let client; beforeEach(() => { client = new Datastore(); client.core._axios = { request: jest.fn() }; }); afterEach(() => { jest.restoreAllMocks(); }); describe('constructor', () => { it('creates a new client with default configuration', () => { client = new Datastore(); expect(client.config).toEqual({ baseUrl: 'http://localhost:3001', timeout: 10000, token: 'token', debug: false, connector: 'http', walk: { maxPageSize: Infinity }, }); }); it('creates a new client with walk max pageSize', () => { client = new Datastore({ walk: { maxPageSize: 100 } }); expect(client.config).toEqual({ baseUrl: 'http://localhost:3001', timeout: 10000, token: 'token', debug: false, connector: 'http', walk: { maxPageSize: 100 }, }); }); it('creates a new client with partial walk configuration', () => { client = new Datastore({ walk: {} }); expect(client.config).toEqual({ baseUrl: 'http://localhost:3001', timeout: 10000, token: 'token', debug: false, connector: 'http', walk: { maxPageSize: Infinity }, }); }); it('creates a new client with configuration defined in the signature', () => { client = new Datastore({ baseUrl: 'https://datastore.org', token: 'private_token', }); expect(client.config).toMatchObject({ baseUrl: 'https://datastore.org', token: 'private_token', }); }); it('creates a new client with axios instanciated', () => { client = new Datastore(); expect(client.core._axios).toHaveProperty('request'); expect(client.core._axios).toHaveProperty('get'); expect(client.core._axios).toHaveProperty('post'); expect(client.core._axios).toHaveProperty('put'); expect(client.core._axios).toHaveProperty('delete'); expect(client.core._axios.defaults.headers).toMatchObject({ 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'token', }); }); it('creates a new client with telemetry enabled', () => { client = new Datastore({ telemetry: { logger: true } }); expect(client.telemetry).toEqual({ logger: true }); }); }); describe('#heartbeat', () => { it('calls the heartbeat route', async () => { await client.heartbeat(); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/heartbeat', headers: { 'force-primary': undefined, }, }); }); }); /** * Admin routes */ describe('#getModels', () => { it('calls the admin get models route', async () => { await client.getModels(); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/admin', headers: { 'force-primary': undefined, }, }); }); }); describe('#getGraph', () => { it('calls the admin get models route', async () => { await client.getGraph(); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/admin/graph', headers: { 'force-primary': undefined, }, }); }); }); describe('#getModel', () => { it('calls the admin get a single model configuration', async () => { await client.getModel('users'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/admin', params: { model: 'users' }, headers: { 'force-primary': undefined, }, }); }); }); describe('#rotateEncryptionKeys', () => { it('calls the admin route to perform a encryption key rotation without selected models', async () => { await client.rotateEncryptionKeys(); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/admin/rotate/keys', params: {}, headers: { 'force-primary': undefined, }, }); }); it('calls the admin route to perform a encryption key rotation on some models only', async () => { await client.rotateEncryptionKeys(['users']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/admin/rotate/keys', params: { models: ['users'] }, headers: { 'force-primary': undefined, }, }); }); }); describe('#createModel', () => { it('calls the admin create model route', async () => { await client.createModel({ db: 'datastore', name: 'users' }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/admin', data: { db: 'datastore', name: 'users' }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no model name is provided', async () => { let error; try { await client.createModel({ db: 'datastore', name: undefined }); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_MODEL_NAME); }); }); describe('#updateModel', () => { it('calls the admin update model route', async () => { await client.updateModel({ db: 'datastore', name: 'users' }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/admin/users', data: { db: 'datastore', name: 'users' }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no model name is provided', async () => { let error; try { await client.updateModel({ db: 'datastore', name: undefined }); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_MODEL_NAME); }); }); describe('#createModelIndexes', () => { it('calls the admin create model indexes route', async () => { await client.createModelIndexes({ db: 'datastore', name: 'users' }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/admin/users/indexes', data: { db: 'datastore', name: 'users' }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no model name is provided', async () => { let error; try { await client.createModelIndexes({ db: 'datastore', name: undefined }); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_MODEL_NAME); }); }); describe('#getSchema', () => { it('calls the admin get model schema route', async () => { await client.getSchema('users'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/admin/users/schema', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no model name is provided', async () => { let error; try { await client.getSchema(undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_MODEL_NAME); }); }); /** * Non admin routes */ describe('#decrypt', () => { it('calls the decrypt model route', async () => { await client.decrypt('users', [{ firstname: 'John' }]); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/decrypt', params: { fields: [] }, data: [{ firstname: 'John' }], headers: { 'force-primary': undefined, }, }); }); it('calls the decrypt model route with additional fields', async () => { await client.decrypt('users', [{ firstname: 'John' }], ['firstname']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/decrypt', params: { fields: ['firstname'] }, data: [{ firstname: 'John' }], headers: { 'force-primary': undefined, }, }); }); }); describe('#encrypt', () => { it('calls the encrypt model route', async () => { await client.encrypt('users', [{ firstname: 'John' }]); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/encrypt', params: { fields: [] }, data: [{ firstname: 'John' }], headers: { 'force-primary': undefined, }, }); }); it('calls the encrypt model route with additional fields', async () => { await client.encrypt('users', [{ firstname: 'John' }], ['firstname']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/encrypt', params: { fields: ['firstname'] }, data: [{ firstname: 'John' }], headers: { 'force-primary': undefined, }, }); }); }); describe('#create', () => { it('calls the create model route', async () => { await client.create('users', { firstname: 'John' }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users', data: { firstname: 'John' }, headers: { 'force-primary': undefined, }, }); }); }); describe('#apply', () => { it('calls the apply model route', async () => { await client.apply( 'users', 'user_correlation_id', 'FIRSTNAME_UPDATED', 0, { firstname: 'Jack' }, ); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/firstname_updated/0', data: { firstname: 'Jack' }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.apply('users', undefined, 'FIRSTNAME_UPDATED', 0, { firstname: 'Jack', }); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#update', () => { it('calls the update model route', async () => { await client.update('users', 'user_correlation_id', { firstname: 'Jack', }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id', data: { firstname: 'Jack' }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.update('users', undefined, { firstname: 'Jack' }); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#patch', () => { it('calls the patch model route', async () => { await client.patch('users', 'user_correlation_id', [ { op: 'replace', path: '/firstname', value: 'Jack' }, ]); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'patch', url: '/api/users/user_correlation_id', data: { json_patch: [{ op: 'replace', path: '/firstname', value: 'Jack' }], }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.patch('users', undefined, [ { op: 'replace', path: '/firstname', value: 'Jack' }, ]); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); it('throws an error if no JSON Patch is provided', async () => { let error; try { await client.patch('users', 'user_id'); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_JSON_PATCH); }); }); describe('#get', () => { it('calls the get model route', async () => { await client.get('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.get('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#count', () => { it('returns the number of entities associated with the query', async () => { client.core._axios = { request: jest .fn() .mockImplementation(() => ({ headers: { count: '1' } })), }; const count = await client.count('users', { firstname: 'Jack' }); expect(count).toEqual(1); expect(client.core._axios.request).toHaveBeenCalledWith({ headers: { page: 0, 'page-size': 0, 'force-primary': undefined }, method: 'get', params: { firstname: 'Jack' }, url: '/api/users', }); }); it('returns the number of entities associated with the query with `entities` source defined', async () => { client.core._axios = { request: jest .fn() .mockImplementation(() => ({ headers: { count: '1' } })), }; const count = await client.count( 'users', { firstname: 'Jack' }, 'entities', ); expect(count).toEqual(1); expect(client.core._axios.request).toHaveBeenCalledWith({ headers: { page: 0, 'page-size': 0, 'force-primary': undefined }, method: 'get', params: { firstname: 'Jack' }, url: '/api/users', }); }); it('returns the number of events associated with the query', async () => { client.core._axios = { request: jest .fn() .mockImplementation(() => ({ headers: { count: '1' } })), }; const count = await client.count( 'users', { firstname: 'Jack' }, 'events', ); expect(count).toEqual(1); expect(client.core._axios.request).toHaveBeenCalledWith({ headers: { page: 0, 'page-size': 0 }, method: 'get', params: { firstname: 'Jack' }, url: '/api/users/events', }); }); }); describe('#find', () => { it('calls the find model route', async () => { await client.find('users', { firstname: 'Jack' }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users', params: { firstname: 'Jack' }, headers: {}, }); }); it('calls the find model route with a page index defined', async () => { await client.find('users', { firstname: 'Jack' }, 1); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users', params: { firstname: 'Jack' }, headers: { page: 1 }, }); }); it('calls the find model route with a page size defined', async () => { await client.find('users', { firstname: 'Jack' }, 1, 10); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users', params: { firstname: 'Jack' }, headers: { page: 1, 'page-size': 10 }, }); }); }); describe('#events', () => { it('calls the get events model route', async () => { await client.events('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/events', headers: {}, }); }); it('calls the get events model route if a page index defined', async () => { await client.events('users', 'user_correlation_id', 1); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/events', headers: { page: 1 }, }); }); it('calls the get events model route if a page size defined', async () => { await client.events('users', 'user_correlation_id', 1, 5); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/events', headers: { page: 1, 'page-size': 5 }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.events('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#allEvents', () => { it('calls the get all events model route', async () => { await client.allEvents('users'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: {}, headers: {}, }); }); it('calls the get all events model route with a specific query', async () => { await client.allEvents('users', { a: 1 }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: { a: 1 }, headers: {}, }); }); it('calls the get all events model route if a page index defined', async () => { await client.allEvents('users', {}, 1); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: {}, headers: { page: 1 }, }); }); it('calls the get all events model route if a page size defined', async () => { await client.allEvents('users', {}, 1, 5); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: {}, headers: { page: 1, 'page-size': 5 }, }); }); }); describe('#version', () => { it('calls the get version model route', async () => { await client.version('users', 'user_correlation_id', 2); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/2', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.version('users', undefined, 2); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); it('returns null data on 404 response error', async () => { client.core._axios.request.mockImplementation(() => { const err = new Error('404 Error'); // @ts-ignore err.response = { status: 404 }; throw err; }); const res = await client.version('users', 'user_correlation_id', 2); expect(res).toEqual({ data: null, status: 404 }); }); it('throws on any other error', async () => { const _err = new Error('404 Error'); // @ts-ignore _err.response = { status: 500 }; client.core._axios.request.mockImplementation(() => { throw _err; }); let error; try { await client.version('users', 'user_correlation_id', 2); } catch (err) { error = err; } expect(error).toEqual(_err); }); }); describe('#firstEventVersion', () => { it('calls the all events route with query', async () => { client.core._axios.request.mockImplementation(() => ({ data: [{ version: 1 }], })); await client.firstEventVersion('users', {}, {}); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: { _fields: { version: 1 }, _sort: {} }, headers: { page: 0, 'page-size': 1, 'force-primary': undefined }, }); }); it('returns default value if none is returned', async () => { client.core._axios.request.mockImplementation(() => ({ data: [] })); const version = await client.firstEventVersion('users', {}, {}, 0); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: { _fields: { version: 1 }, _sort: {} }, headers: { page: 0, 'page-size': 1 }, }); expect(version).toEqual(0); }); it('changes queries from entities on events compatible ones', async () => { client.core._axios.request.mockImplementation(() => ({ data: [{ version: 1 }], })); await client.firstEventVersion( 'users', { updated_at: { 'date($gt)': '2020-01-01T00:00:00.000Z' } }, {}, ); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/events', params: { created_at: { 'date($gt)': '2020-01-01T00:00:00.000Z' }, _fields: { version: 1 }, _sort: {}, }, headers: { page: 0, 'page-size': 1 }, }); }); }); describe('#at', () => { it('calls the get timetravel model route with a date parameter', async () => { await client.at( 'users', 'user_correlation_id', '2020-01-01T00:00:00.000Z', ); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/2020-01-01T00:00:00.000Z', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.version('users', undefined, 2); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#restore', () => { it('calls the restore model route', async () => { await client.restore('users', 'user_correlation_id', 1); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/1/restore', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.restore('users', undefined, 1); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#snapshot', () => { it('calls the create snapshot model route', async () => { await client.snapshot('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/snapshot', headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.snapshot('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#data', () => { it('calls the data model route', async () => { await client.data('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/data', params: {}, headers: { 'force-primary': undefined, }, }); }); it('calls the dqtq model route with models', async () => { await client.data('users', 'user_correlation_id', ['devices']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'get', url: '/api/users/user_correlation_id/data', params: { models: ['devices'] }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.data('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#archive', () => { it('calls the archive model route', async () => { await client.archive('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/archive', params: { deep: false }, headers: { 'force-primary': undefined, }, }); }); it('calls the archive model route with deep graph parameter', async () => { await client.archive('users', 'user_correlation_id', true); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/archive', params: { deep: true }, headers: { 'force-primary': undefined, }, }); }); it('calls the archive model route with deep graph parameter', async () => { await client.archive('users', 'user_correlation_id', true, ['devices']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/archive', params: { deep: true, models: ['devices'] }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.archive('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#unarchive', () => { it('calls the create unarchive model route', async () => { await client.unarchive('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/unarchive', params: { deep: false }, headers: { 'force-primary': undefined, }, }); }); it('calls the unarchive model route with deep graph parameter', async () => { await client.unarchive('users', 'user_correlation_id', true); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/unarchive', params: { deep: true }, headers: { 'force-primary': undefined, }, }); }); it('calls the unarchive model route with deep graph parameter', async () => { await client.unarchive('users', 'user_correlation_id', true, ['devices']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/users/user_correlation_id/unarchive', params: { deep: true, models: ['devices'] }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.unarchive('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#delete', () => { it('calls the delete model route', async () => { await client.delete('users', 'user_correlation_id'); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'delete', url: '/api/users/user_correlation_id', params: { deep: false }, headers: { 'force-primary': undefined, }, }); }); it('calls the delete model route with deep graph parameter', async () => { await client.delete('users', 'user_correlation_id', true); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'delete', url: '/api/users/user_correlation_id', params: { deep: true }, headers: { 'force-primary': undefined, }, }); }); it('calls the unarchive model route with deep graph parameter', async () => { await client.delete('users', 'user_correlation_id', true, ['devices']); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'delete', url: '/api/users/user_correlation_id', params: { deep: true, models: ['devices'] }, headers: { 'force-primary': undefined, }, }); }); it('throws an error if no correlation ID is provided', async () => { let error; try { await client.delete('users', undefined); } catch (err) { error = err; } expect(error).toEqual(ERROR_MISSING_CORRELATION_ID); }); }); describe('#walkMulti', () => { it('walks over multiple queries', async () => { jest .spyOn(client, 'firstEventVersion') .mockImplementation(() => ({ data: 0 })); jest.spyOn(client, 'walkNext').mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await Datastore.walkMulti( new Map([['datastore', client]]), [ { datastore: 'datastore', model: 'users', query: {}, source: 'entities', }, ], handler, ); expect(handler).toHaveBeenCalledTimes(3); expect(entities).toEqual([ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ]); }); }); describe('#walk', () => { it('walks over every entity available in the datastore', async () => { jest .spyOn(client, 'firstEventVersion') .mockImplementation(() => ({ data: 0 })); jest.spyOn(client, 'walkNext').mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler); expect(handler).toHaveBeenCalledTimes(3); expect(entities).toEqual([ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ]); }); it('walks with a default pageSize of 10 by default', async () => { jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); const walkNextMock = jest .spyOn(client, 'walkNext') .mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler); expect(walkNextMock).toHaveBeenCalledWith( 'users', {}, 'entities', 0, 20, // = 2 * 10 { current_version: -1, cursor_last_correlation_id: '', cursor_last_id: '', headers: undefined, version_ordered: false, }, ); }); it('walks with a specified pageSize if lower of the max pageSize defined in client', async () => { jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); const walkNextMock = jest .spyOn(client, 'walkNext') .mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler, 25); expect(walkNextMock).toHaveBeenCalledWith( 'users', {}, 'entities', 0, 50, // = 2 * 25 { current_version: -1, cursor_last_correlation_id: '', cursor_last_id: '', headers: undefined, version_ordered: false, }, ); }); it('walks with no default max pageSize if not defined', async () => { jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); const walkNextMock = jest .spyOn(client, 'walkNext') .mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler, 1000); expect(walkNextMock).toHaveBeenCalledWith( 'users', {}, 'entities', 0, 2000, // = 2 * 1000 { current_version: -1, cursor_last_correlation_id: '', cursor_last_id: '', headers: undefined, version_ordered: false, }, ); }); it('walks with no default max pageSize if not defined', async () => { client.config.walk = undefined; jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); const walkNextMock = jest .spyOn(client, 'walkNext') .mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler, 1000); expect(walkNextMock).toHaveBeenCalledWith( 'users', {}, 'entities', 0, 2000, // = 2 * 1000 { current_version: -1, cursor_last_correlation_id: '', cursor_last_id: '', headers: undefined, version_ordered: false, }, ); }); it('walks with the defined max pageSize if apply', async () => { client = new Datastore({ walk: { maxPageSize: 13 } }); jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); const walkNextMock = jest .spyOn(client, 'walkNext') .mockImplementation(() => ({ data: [ { a: 1, created_at: '1' }, { a: 2, created_at: '2' }, { a: 3, created_at: '3' }, ], headers: { 'correlation-field': 'a' }, })); const entities: any[] = []; const handler = jest .fn() .mockImplementation((entity) => entities.push(entity)); await client.walk('users', {}, handler, 1000); expect(walkNextMock).toHaveBeenCalledWith( 'users', {}, 'entities', 0, 26, // = 2 * 13 { current_version: -1, cursor_last_correlation_id: '', cursor_last_id: '', headers: undefined, version_ordered: false, }, ); }); }); describe('#aggregate', () => { it('calls the aggregate special route', async () => { await client.aggregate([]); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/aggregate', data: [], headers: { 'force-primary': undefined, }, }); }); it('calls the aggregate special route with headers', async () => { await client.aggregate([], { timeout: 10000 }); expect(client.core._axios.request).toHaveBeenLastCalledWith({ method: 'post', url: '/api/aggregate', data: [], headers: { timeout: 10000 }, }); }); }); describe('⚠ deprecated ⚠', () => { // Deprecated methods describe('⚠ #stream', () => { let close; let eventSource; beforeEach(() => { client.streams.getEventSource = jest.fn().mockImplementation(() => { eventSource = new EventEmitter(); eventSource.addEventListener = eventSource.on; eventSource.close = jest.fn(); setTimeout(() => eventSource.emit('open'), 10); return eventSource; }); }); afterEach(() => { jest.restoreAllMocks(); close(); }); it('calls the stream route with default parameters', async () => { const handler = jest.fn(); close = await client.stream(handler); expect(client.streams.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/all/entities/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given model', async () => { const handler = jest.fn(); close = await client.stream(handler, 'users'); expect(client.streams.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/entities/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given model and for events', async () => { const handler = jest.fn(); close = await client.stream(handler, 'users', 'events'); expect(client.streams.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/events/sse?pipeline=[]', { authorization: 'token' }, ); }); it('calls the stream route for a given aggregation pipeline', async () => { const handler = jest.fn(); close = await client.stream(handler, 'users', 'entities', [ { 'fullDocument.email': 'john' }, ]); expect(client.streams.getEventSource).toHaveBeenCalledWith( 'http://localhost:3001/api/stream/users/entities/sse?pipeline=[{"fullDocument.email":"john"}]', { authorization: 'token' }, ); }); it('invokes the handler on JSON object reception', async () => { const handler = jest.fn(); close = await client.stream(handler); eventSource.emit('message', { data: JSON.stringify({ a: 1 }) }); expect(handler).toHaveBeenCalledWith({ a: 1 }); }); it('closes the connection on the invokation of the close returned handler', async () => { const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); const handler = jest.fn(); close = await client.stream(handler); close(); expect(eventSource.close).toHaveBeenCalledTimes(1); }); }); describe('⚠ #listen / #close', () => { let close; let eventSource; beforeEach(() => { close = jest.fn(); client.streams.getEventSource = jest.fn().mockImplementation(() => { eventSource = new EventEmitter(); eventSource.addEventListener = eventSource.on; eventSource.close = jest.fn(); setTimeout(() => eventSource.emit('open'), 10); return eventSource; }); }); afterEach(() => { close(); }); it('allows to listen events from the Datastore', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); const firstCallArgumentsExceptHandler = client.streams.streamHTTP.mock.calls[0]; firstCallArgumentsExceptHandler.shift(); expect(firstCallArgumentsExceptHandler).toEqual([ 'all', 'events', undefined, { forward: client }, ]); }); it('allows to listen events based on a customized projection', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events', { state: 'created' }); const firstCallArgumentsExceptHandler = client.streams.streamHTTP.mock.calls[0]; firstCallArgumentsExceptHandler.shift(); expect(firstCallArgumentsExceptHandler).toEqual([ 'all', 'events', { state: 'created' }, { forward: client }, ]); }); it('binds only one stream per streamId', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); await client.listen('all', 'events'); expect(client.streams.streamHTTP).toHaveBeenCalledTimes(1); }); it('registers a stream with a streamId defined with query JSON stringified', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events', { test: 1 }); expect(Array.from(client.streams._streams.keys())).toEqual([ 'all:events:{"test":1}', ]); expect(client.streams._streams.get('all:events:{"test":1}')).toEqual( close, ); }); it('registers a new stream in the Datastore client', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); expect(client.streams._streams.get('all:events:{}')).toEqual(close); }); it('closes a stream connection if it exists on close method call', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); client.close(client.streams.getStreamId('all', 'events')); expect(close).toHaveBeenCalledTimes(1); }); it('closes all stream connections on closeAll invokation', async () => { client.streams.streamHTTP = jest.fn().mockImplementation(() => close); await client.listen('all', 'events'); await client.listen('all', 'events', { test: 1 }); expect(Array.from(client.streams._streams.keys())).toEqual([ 'all:events:{}', 'all:events:{"test":1}', ]); client.closeAll(); expect(close).toHaveBeenCalledTimes(2); expect(client.streams._streams.size).toEqual(0); }); it('emits a message on the streamId ', async () => { await client.listen('all', 'events'); let message; client.on('all:events:{}', (e) => (message = e)); eventSource.emit('message', { data: JSON.stringify({ a: 1 }) }); expect(message).toEqual({ a: 1 }); }); }); }); });