UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

2,227 lines (2,014 loc) 71.2 kB
import * as utils from '../../utils'; import Datastore from '../Datastore'; import Aggregator from './Aggregator'; describe('sdk/Aggregator', () => { const DateNow = Date.now; let client; let aggregator; beforeEach(() => { Date.now = jest .fn() .mockImplementation(() => new Date('2021-01-01T00:00:00.000Z').getTime()); client = new Datastore(); client.maxEventsVersion = jest.fn().mockResolvedValue(1); client.axios = { request: jest.fn(), }; const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); }); afterEach(() => { Date.now = DateNow; jest.restoreAllMocks(); }); describe('constructor', () => { it('creates a new aggregator with datastores', () => { const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); expect(aggregator).toBeInstanceOf(Aggregator); expect(aggregator.datastores).toEqual(datastores); }); it('creates a new aggregator with configuration', () => { const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores, { max_retry: 1, }); expect(aggregator.config).toEqual({ max_retry: 1, }); }); }); describe('ok', () => { it('performs a noop if the condition is `true`', () => { let error = null; try { Aggregator.ok(true, 'My message'); } catch (err) { error = err; } expect(error).toEqual(null); }); it('throws an exception if the condition is `false`', () => { let error = null; try { Aggregator.ok(false, 'My message'); } catch (err) { error = err; } expect(error).toEqual(new Error('My message')); }); }); describe('#validate', () => { it('validates pipeline contract', () => { const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores, { max_retry: 1, }); let error; try { aggregator.validate([ { type: 'fetch', model: 'accounts', map: [ { from: 'test', to: 'test', default: {}, }, ], }, ]); } catch (err) { error = err; } expect(error).toEqual(undefined); }); it('throws an error if the pipeline is invalid', () => { const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores, { max_retry: 1, }); let error; try { aggregator.validate([ { type: 'invalid', model: 'accounts', map: [ { from: 'test', to: 'test', default: {}, }, ], }, ]); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_INVALID_PIPELINE_DEFINITION); }); }); describe('#addStepType', () => { it('adds a new step to the aggregator', () => { const stepDef = { handler: jest.fn(), }; const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); aggregator.addStepType('my_step', stepDef); expect(aggregator.steps.get('my_step')).toEqual(stepDef); }); it('throws an error in case of step type prior existence', () => { const stepFn = jest.fn(); const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); aggregator.addStepType('my_step', stepFn); let error; try { aggregator.addStepType('my_step', stepFn); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_CONFLICT_STEP_TYPE); }); }); describe('#removeStepType', () => { it('removes a step type from the list', () => { const stepFn = jest.fn(); const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); aggregator.addStepType('my_step', stepFn); aggregator.removeStepType('my_step'); expect(aggregator.steps.has('my_step')).toEqual(false); }); it('noop if the step type does not exist', () => { const datastores = new Map([['datastore', client]]); aggregator = new Aggregator(datastores); aggregator.removeStepType('my_step'); expect(aggregator.steps.has('my_step')).toEqual(false); }); }); describe('#applyMap', () => { it('returns a default empty query if none is defined in the step', () => { const query = aggregator.applyMap({}); expect(query).toEqual({}); }); it('returns the query defined in the step if none is defined', () => { const query = aggregator.applyMap({ firstname: 'john', }); expect(query).toEqual({ firstname: 'john', }); }); it('returns the query built with previous data if `map` is defined', () => { const query = aggregator.applyMap( {}, [ { from: 'profile.firstname', to: 'firstname', }, ], { profile: { firstname: 'john', }, }, ); expect(query).toEqual({ firstname: 'john', }); }); it('returns the query built with a relative timing with `relative_date_in_seconds`', () => { const query = aggregator.applyMap( {}, [ { from: 'profile.created_at', to: ['created_at', 'date($gt)'], relative_date_in_seconds: -3600, }, ], { profile: { created_at: '2021-02-01', }, }, ); expect(query).toEqual({ created_at: { 'date($gt)': new Date('2021-01-31T23:00:00.000Z'), }, }); }); it('returns the query built with a relative timing with `relative_date_in_seconds` without date', () => { const query = aggregator.applyMap( {}, [ { from: '_', to: ['created_at', 'date($gt)'], relative_date_in_seconds: -3600, }, ], { profile: { created_at: '2021-02-01', }, }, ); expect(query).toEqual({ created_at: { 'date($gt)': new Date('2020-12-31T23:00:00.000Z'), }, }); }); it('returns the hash value of the string value', () => { const query = aggregator.applyMap( {}, [ { from: 'profile.firstname', to: 'firstname', must_hash: true, }, ], { profile: { firstname: 'john', }, }, ); expect(query).toEqual({ firstname: 'b7fcc6e612145267d2ffea04be754a34128c1ed8133a09bfbbabd6afe6327688aa71d47343dd36e719f35f30fa79aec540e91b81c214fddfe0bedd53370df46d', }); }); it('returns the hash value of the stringified value', () => { const query = aggregator.applyMap( {}, [ { from: 'profile', to: 'profile', must_hash: true, json_stringify: true, }, ], { profile: { firstname: 'john', }, }, ); expect(query).toEqual({ profile: '79d299cb21cc50cbbd4abb18885ccc602e88cc3993faf160b68c5bdb2b941794063b31e2b1c5fda1787118dfae75afea4f604ad3f5ad7fc7dd7b7a946ce5274a', }); }); it('returns the query built with default value if `map` and `default` are defined and no value is found', () => { const query = aggregator.applyMap( {}, [ { from: 'profile.firstname', to: 'firstname', default: 'unknown', }, ], { profile: { // firstname: 'john', // <- missing value }, }, ); expect(query).toEqual({ firstname: 'unknown', }); }); it('returns undefined on unitialized data', () => { const query = aggregator.applyMap({}, [ { from: 'profile.firstname', to: 'firstname', default: 'unknown', }, ]); expect(query).toEqual({ firstname: 'unknown', }); }); it('replaces the object with the targeted value if `to` is `.`', () => { const query = aggregator.applyMap( {}, [ { from: 'profile.firstname', to: '.', default: 'unknown', }, ], { profile: { firstname: 'john', }, }, ); expect(query).toEqual('john'); }); it('copy the data to the result if from is equal to `.`', () => { const query = aggregator.applyMap( {}, [ { from: '.', to: '.', }, ], { profile: { firstname: 'john', }, }, ); expect(query).toEqual({ profile: { firstname: 'john', }, }); }); it('applies the default value to the result if from is equal to `.` and no data is provided', () => { const query = aggregator.applyMap({}, [ { from: '.', to: '.', default: { profile: { firstname: 'john', }, }, }, ]); expect(query).toEqual({ profile: { firstname: 'john', }, }); }); }); describe('#fetch', () => { it('queries the first available datastore if none is defined', async () => { jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); jest.spyOn(client, 'walkNext').mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ model: 'profiles' }); expect(client.walkNext).toHaveBeenCalledWith( 'profiles', {}, 'entities', 0, 20, { current_version: -1, cursor_last_id: '', cursor_last_correlation_id: '', headers: undefined, version_ordered: false, }, ); }); it('returns all results associated to a query', async () => { jest.spyOn(client, 'firstEventVersion').mockImplementation(() => 0); jest .spyOn(client, 'walkNext') .mockImplementationOnce(() => ({ data: [ { a: 1, created_at: '1', }, { a: 2, created_at: '2', }, { a: 3, created_at: '3', }, ], headers: { page: 0, 'page-size': 10, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', query: { is_enabled: true, }, }); expect(results).toEqual([ { a: 1, created_at: '1', }, { a: 2, created_at: '2', }, { a: 3, created_at: '3', }, ]); }); it('returns second page of results associated to a query if `page` is defined', async () => { jest .spyOn(client, 'find') .mockImplementationOnce(() => ({ data: [ { a: 1, }, ], headers: { page: 1, 'page-size': 10, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', page: 1, query: { is_enabled: true, }, }); expect(results).toEqual([ { a: 1, }, ]); expect(client.find).toHaveBeenCalledWith( 'profiles', { is_enabled: true }, 1, undefined, undefined, ); }); it('returns first page with a page size of results associated to a query if `page_size` is defined', async () => { client.find = jest .fn() .mockImplementationOnce((model, query, page, pageSize) => ({ data: [ { a: 1, }, ], headers: { page, 'page-size': pageSize, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', page_size: 10, query: { is_enabled: true, }, }); expect(results).toEqual([ { a: 1, }, ]); expect(client.find).toHaveBeenCalledWith( 'profiles', { is_enabled: true }, undefined, 10, undefined, ); }); it('returns second page of events of results associated to a query if `page` is defined', async () => { client.allEvents = jest .fn() .mockImplementationOnce(() => ({ data: [ { type: 'CREATED', a: 1, }, ], headers: { page: 1, 'page-size': 10, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', source: 'events', page: 1, query: { is_enabled: true, }, }); expect(results).toEqual([ { type: 'CREATED', a: 1, }, ]); expect(client.allEvents).toHaveBeenCalledWith( 'profiles', { is_enabled: true }, 1, undefined, undefined, ); }); it('returns first page of events with a page size of results associated to a query if `page_size` is defined', async () => { client.allEvents = jest .fn() .mockImplementationOnce(() => ({ data: [ { type: 'CREATED', a: 1, }, ], headers: { page: 1, 'page-size': 10, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', source: 'events', page_size: 10, query: { is_enabled: true, }, }); expect(results).toEqual([ { type: 'CREATED', a: 1, }, ]); expect(client.allEvents).toHaveBeenCalledWith( 'profiles', { is_enabled: true }, undefined, 10, undefined, ); }); it('returns the decrypted entities if requested', async () => { client.decrypt = jest.fn().mockImplementation(() => ({ data: [ { firstname: 'Alice', }, ], })); client.find = jest .fn() .mockImplementationOnce(() => ({ data: [{ firstname: 'encrypted' }], headers: { page: 0, 'page-size': 10, }, })) .mockImplementation(() => ({ data: [], headers: { page: 1, 'page-size': 10, }, })); const results = await aggregator.fetch({ datastore: 'datastore', model: 'profiles', must_decrypt: true, page: 0, query: { is_enabled: true, }, }); expect(results).toEqual([ { firstname: 'Alice', }, ]); }); it('returns the entity from timetravel if requested (walk)', async () => { client.at = jest.fn().mockImplementation(() => ({ data: { profile_id: 'alice', firstname: 'Alice 0', version: 0 }, })); client.find = jest .fn() .mockImplementationOnce((model, query, page, pageSize) => ({ data: [ { profile_id: 'alice', firstname: 'Alice 1', version: 1, }, ], headers: { page, 'page-size': pageSize, }, })); const results = await aggregator.fetch( { datastore: 'datastore', model: 'profiles', correlation_field: 'profile_id', timetravel: 'entity.created_at', page: 0, query: { is_enabled: true, }, }, { entity: { created_at: new Date('2021-01-01T00:00:00.000Z').toISOString(), }, }, ); expect(results).toEqual([ { firstname: 'Alice 0', profile_id: 'alice', version: 0, }, ]); expect(client.at).toHaveBeenCalledWith( 'profiles', 'alice', '2021-01-01T00:00:00.000Z', ); }); it('throws a timetravel exception if no `correlation_field` is specified', async () => { client.at = jest.fn().mockImplementation(() => ({ data: { profile_id: 'alice', firstname: 'Alice 0', version: 0 }, })); client.find = jest .fn() .mockImplementationOnce((model, query, page, pageSize) => ({ data: [ { profile_id: 'alice', firstname: 'Alice 1', version: 1, }, ], headers: { page, 'page-size': pageSize, }, })); let error; try { const results = await aggregator.fetch( { datastore: 'datastore', model: 'profiles', timetravel: 'entity.created_at', page: 0, query: { is_enabled: true, }, }, { entity: { created_at: new Date(2021, 0, 1).toISOString(), }, }, ); } catch (err) { error = err; } expect(error).toEqual(new Error('Invalid timetravel condition')); }); it('throws an error if no timetravel date can be found from state', async () => { client.at = jest.fn().mockImplementation(() => ({ data: { profile_id: 'alice', firstname: 'Alice 0', version: 0 }, })); client.find = jest .fn() .mockImplementationOnce((model, query, page, pageSize) => ({ data: [ { profile_id: 'alice', firstname: 'Alice 1', version: 1, }, ], headers: { page, 'page-size': pageSize, }, })); let error; try { const results = await aggregator.fetch( { datastore: 'datastore', model: 'profiles', correlation_field: 'profile_id', timetravel: 'entity.invalid', page: 0, query: { is_enabled: true, }, }, { entity: { created_at: new Date(2021, 0, 1).toISOString(), }, }, ); } catch (err) { error = err; } expect(error).toEqual(new Error('Invalid timetravel condition')); }); }); describe('#persist', () => { beforeEach(() => { client.create = jest.fn().mockImplementation(() => ({ data: { created: true }, })); client.update = jest.fn().mockImplementation(() => ({ data: { updated: true }, })); }); it('persists an empty fragment into a datastore', async () => { const result = await aggregator.persist({ model: 'profiles', }); expect(client.create).toHaveBeenCalledWith('profiles', {}, {}); }); it('persists an empty fragment into a specific datastore', async () => { const result = await aggregator.persist({ model: 'profiles', datastore: 'datastore', }); expect(client.create).toHaveBeenCalledWith('profiles', {}, {}); expect(client.update).not.toHaveBeenCalled(); }); it('returns the created entity', async () => { const result = await aggregator.persist({ model: 'profiles', destination: 'entity', }); expect(result).toEqual({ created: true }); }); it('persists a fragment with defined payload into a datastore', async () => { const result = await aggregator.persist({ model: 'profiles', payload: { firstname: 'John', }, }); expect(client.update).not.toHaveBeenCalled(); expect(client.create).toHaveBeenCalledWith( 'profiles', { firstname: 'John', }, {}, ); }); it('persists a fragment with headers into a datastore', async () => { const result = await aggregator.persist({ model: 'profiles', payload: { firstname: 'John', }, headers: { upsert: true, }, }); expect(client.update).not.toHaveBeenCalled(); expect(client.create).toHaveBeenCalledWith( 'profiles', { firstname: 'John', }, { upsert: true, }, ); }); it('persists a fragment with `created_at` header if provided into the payload', async () => { const result = await aggregator.persist({ model: 'profiles', payload: { firstname: 'John', created_at: '2020-01-01T00:00:00.000Z', }, headers: { upsert: true, }, }); expect(client.update).not.toHaveBeenCalled(); expect(client.create).toHaveBeenCalledWith( 'profiles', { firstname: 'John', }, { upsert: true, 'created-at': '2020-01-01T00:00:00.000Z', }, ); }); it('persists a fragment with a mapping payload logic', async () => { const result = await aggregator.persist( { model: 'profiles', map: [ { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { firstname: 'John', }, }, ); expect(client.create).toHaveBeenCalledWith( 'profiles', { firstname: 'John', }, {}, ); }); it('updates en entity if a `correlation_field` is defined and available', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, {}, ); }); it('updates en entity preserving the `updated_at` value', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', map: [ { from: 'profile.updated_at', to: 'updated_at', }, { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', updated_at: '2020-01-01T00:00:00.000Z', }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, { 'created-at': '2020-01-01T00:00:00.000Z', }, ); }); it('updates en entity preserving the `created_at` value of the event', async () => { const result = await aggregator.persist( { model: 'profiles', source: 'events', correlation_field: 'profile_id', map: [ { from: 'event.created_at', to: 'created_at', }, { from: 'event.profile_id', to: 'profile_id', }, { from: 'event.firstname', to: 'firstname', }, ], }, { event: { profile_id: 'profile_id', firstname: 'John', type: 'CREATED', created_at: '2020-01-01T00:00:00.000Z', }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, { 'created-at': '2020-01-01T00:00:00.000Z', }, ); }); it('returns the updated entity', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', }, }, ); expect(result).toEqual({ updated: true }); }); it('updates en entity if a `correlation_field` is defined, available and with headers', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', headers: { upsert: true, }, map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, { upsert: true, }, ); }); it('updates an entity with an imperative version if defined', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', imperative_version_next: 'profile.version', headers: { upsert: true, }, map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', version: 1, }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, { upsert: true, version: 2, }, ); }); it('updates an entity with an imperative version to 0 if path not found', async () => { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', imperative_version_next: 'path.not.found', headers: { upsert: true, }, map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { profile_id: 'profile_id', firstname: 'John', version: 1, }, }, ); expect(client.update).toHaveBeenCalledWith( 'profiles', 'profile_id', { profile_id: 'profile_id', firstname: 'John', }, { upsert: true, version: 0, }, ); }); it('throws an exception if a `correlation_field` is defined but not found', async () => { let error; try { const result = await aggregator.persist( { model: 'profiles', correlation_field: 'profile_id', map: [ { from: 'profile.profile_id', to: 'profile_id', }, { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { firstname: 'John', }, }, ); } catch (err) { error = err; } expect(error).toEqual(new Error('Correlation ID must be null or exist')); }); }); describe('#mergeData', () => { it('returns the data initialized with an empty object', () => { const data = aggregator.mergeData({ destination: 'counts', as_entity: false, default: [ { count: 0, }, ], }); expect(data).toEqual({ counts: [ { count: 0, }, ], }); }); it('returns the default objects if no results are provided and defaults are defined', () => { const data = aggregator.mergeData( { destination: 'counts', as_entity: false, default: [ { count: 0, }, ], }, { firstname: 'John', }, ); expect(data).toEqual({ firstname: 'John', counts: [ { count: 0, }, ], }); }); it('returns an empty array if no results are provided not default value', () => { const data = aggregator.mergeData( { destination: 'profiles', as_entity: false, }, { firstname: 'John', }, ); expect(data).toEqual({ firstname: 'John', profiles: [], }); }); it('returns the default object if no result found and a default value is defined', () => { const data = aggregator.mergeData( { destination: 'count', as_entity: true, default: 0, }, { firstname: 'John', }, ); expect(data).toEqual({ firstname: 'John', count: 0, }); }); it('returns a default object with results linked', () => { const data = aggregator.mergeData( { destination: 'profile', as_entity: true, }, {}, [ { firstname: 'John', }, ], ); expect(data).toEqual({ profile: { firstname: 'John', }, }); }); it('returns the data with all results stored in the destination', () => { const data = aggregator.mergeData( { destination: 'profiles', }, {}, [ { firstname: 'Alice', }, { firstname: 'Bernard', }, ], ); expect(data).toEqual({ profiles: [ { firstname: 'Alice', }, { firstname: 'Bernard', }, ], }); }); it('adds entries in the data object if an array already exists', () => { const data = aggregator.mergeData( { destination: 'profiles', }, { profiles: [ { firstname: 'Alice', }, ], }, [ { firstname: 'Bernard', }, ], ); expect(data).toEqual({ profiles: [ { firstname: 'Alice', }, { firstname: 'Bernard', }, ], }); }); it('adds an object at the root level', () => { const data = aggregator.mergeData( { destination: '.', as_entity: true, }, {}, [ { firstname: 'Alice', }, ], ); expect(data).toEqual({ firstname: 'Alice', }); }); it('throws a not found exception if `as_entity=true` and no results exist', () => { let error; try { const data = aggregator.mergeData( { destination: '.', as_entity: true, }, {}, [], ); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_ENTITY_NOT_FOUND); }); it('throws an error if the destination is not defined', () => { let error; try { const data = aggregator.mergeData({ as_entity: true, }); } catch (err) { error = err; } expect(error).toBeInstanceOf(Error); expect(error).toEqual(Aggregator.ERROR_DESTINATION_UNDEFINED); }); }); describe('#runStepFetch', () => { it('fetches data and updates the aggregation object', async () => { aggregator.fetch = jest.fn().mockImplementation(() => [ { firstname: 'Alice', }, { firstname: 'Bernard', }, ]); const data = await aggregator.runStepFetch({ type: 'fetch', datastore: 'datastore', model: 'profiles', destination: 'profiles', query: {}, }); expect(data).toEqual({ profiles: [ { firstname: 'Alice', }, { firstname: 'Bernard', }, ], }); }); }); describe('#runStepPersist', () => { it('persists data and updates the aggregation object', async () => { aggregator.persist = jest.fn().mockImplementation(() => ({ firstname: 'Alice', })); const data = await aggregator.runStepPersist({ type: 'fetch', datastore: 'datastore', model: 'profiles', destination: 'profile', payload: {}, }); expect(data).toEqual({ profile: { firstname: 'Alice', }, }); }); it('persists data into a default `persist` location', async () => { aggregator.persist = jest.fn().mockImplementation(() => ({ firstname: 'Alice', })); const data = await aggregator.runStepPersist({ type: 'fetch', datastore: 'datastore', model: 'profiles', // destination: 'profile', // <- missing payload: {}, }); expect(data).toEqual({ persist: { firstname: 'Alice', }, }); }); }); describe('#runStepJsonPatch', () => { it('returns the data with JSON Patch applied', async () => { const data = await aggregator.runStepJsonPatch({ type: 'json_patch', patch: [{ op: 'add', path: '/hello', value: 'world' }], }); expect(data).toEqual({ hello: 'world', }); }); }); describe('#runStepMap', () => { it('returns an empty object if not initiliazed', async () => { const data = await aggregator.runStepMap({ type: 'map', map: [ { from: 'profile.firstname', to: 'firstname', }, ], }); expect(data).toEqual({}); }); it('maps data inside the object', async () => { const data = await aggregator.runStepMap( { type: 'map', map: [ { from: 'profile.firstname', to: 'firstname', }, ], }, { profile: { firstname: 'Alice', }, }, ); expect(data).toEqual({ profile: { firstname: 'Alice', }, firstname: 'Alice', }); }); }); describe('#runStepUnset', () => { it('returns empty object', async () => { const data = await aggregator.runStepUnset({ type: 'unset', path: 'profile.lastname', }); expect(data).toEqual({}); }); it('removes a single key from the aggregation', async () => { const data = await aggregator.runStepUnset( { type: 'unset', path: 'profile.lastname', }, { profile: { firstname: 'Alice', lastname: 'Cooper', }, }, ); expect(data).toEqual({ profile: { firstname: 'Alice', }, }); }); }); describe('#runStepValidate', () => { it('validate default empty `data` if not defined', async () => { let error; try { const data = await aggregator.runStepValidate({ type: 'validate', schema: { type: 'string', }, must_throw: true, }); } catch (err) { error = err; } expect(error.message).toEqual( Aggregator.ERROR_VALIDATE_STEP_FAILED.message, ); }); it('validate a sub path if defined', async () => { const data = await aggregator.runStepValidate( { type: 'validate', path: 'sub.count', schema: { type: 'number', enum: [0], }, }, { sub: { count: 0, }, }, ); expect(data).toMatchObject({ validation: { is_valid: true, }, }); }); it('throws an exception if requested', async () => { let error; try { const data = await aggregator.runStepValidate( { type: 'validate', schema: { type: 'string', }, must_throw: true, }, { invalid: true, }, ); } catch (err) { error = err; } expect(error.message).toEqual( Aggregator.ERROR_VALIDATE_STEP_FAILED.message, ); }); it('stores the validation error in the default destination `validation`', async () => { const data = await aggregator.runStepValidate( { type: 'validate', schema: { type: 'string', }, }, { invalid: true, }, ); expect(data).toEqual({ invalid: true, validation: { is_valid: false, errors: [ { instancePath: '', keyword: 'type', message: 'must be string', params: { type: 'string', }, schemaPath: '#/type', }, ], }, }); }); it('stores the validation error in the requested destination', async () => { const data = await aggregator.runStepValidate( { type: 'validate', schema: { type: 'string', }, destination: 'my_destination', }, { invalid: true, }, ); expect(data).toEqual({ invalid: true, my_destination: { is_valid: false, errors: [ { instancePath: '', keyword: 'type', message: 'must be string', params: { type: 'string', }, schemaPath: '#/type', }, ], }, }); }); }); describe('#runStepEach', () => { it('throws an exception on uninitialized data', async () => { let error; try { const data = await aggregator.runStepEach({ type: 'each', path: 'items', destination: 'results', pipeline: [ { type: 'map', map: [ { from: 'id', to: 'new_id', }, ], }, { type: 'unset', path: 'id', }, ], }); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_ITERATE_STEP_IS_NOT_ARRAY); }); it('performs the defined step on each element of an array', async () => { const data = await aggregator.runStepEach( { type: 'each', path: 'items', destination: 'results', pipeline: [ { type: 'map', map: [ { from: 'id', to: 'new_id', }, ], }, { type: 'unset', path: 'id', }, ], }, { items: [ { id: 1, }, { id: 2, }, ], }, ); expect(data).toMatchObject({ results: [ { new_id: 1, }, { new_id: 2, }, ], }); }); it('performs the defined step on the same array', async () => { const data = await aggregator.runStepEach( { type: 'each', path: 'items', pipeline: [ { type: 'map', map: [ { from: 'id', to: 'new_id', }, ], }, { type: 'unset', path: 'id', }, ], }, { items: [ { id: 1, }, { id: 2, }, ], }, ); expect(data).toEqual({ items: [ { new_id: 1, }, { new_id: 2, }, ], }); }); it('throws an exception if the value to iterate on is not an array', async () => { let error; try { const data = await aggregator.runStepEach( { type: 'each', path: 'invalid', destination: 'results', pipeline: [], }, { invalid: {}, }, ); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_ITERATE_STEP_IS_NOT_ARRAY); }); it('throws an exception if no value is found', async () => { let error; try { const data = await aggregator.runStepEach( { type: 'each', path: 'invalid', destination: 'results', pipeline: [], }, { target: [], }, ); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_ITERATE_STEP_IS_NOT_ARRAY); }); }); describe('#runStepIf', () => { it('returns an empty object if the state is noot initialized yet', async () => { const data = await aggregator.runStepIf({ type: 'if', path: 'firstname', schema: { type: 'object', }, }); expect(data).toEqual({}); }); it('returns the current data if the validation part is false', async () => { const data = await aggregator.runStepIf( { type: 'if', path: 'firstname', schema: { type: 'object', }, }, { firstname: 'John', }, ); expect(data).toEqual({ firstname: 'John', }); }); it('returns the current data if the validation part is false without path definition', async () => { const data = await aggregator.runStepIf( { type: 'if', schema: { type: 'object', properties: { firstname: { type: 'object', }, }, }, }, { firstname: 'John', }, ); expect(data).toEqual({ firstname: 'John', }); }); it('invokes the pipeline if the validation part is true', async () => { const data = await aggregator.runStepIf( { type: 'if', path: 'firstname', schema: { type: 'string', }, pipeline: [ { type: 'map', map: [ { from: '_', to: 'lastname', default: 'Doe', }, ], }, ], }, { firstname: 'John', lastname: 'Doe', }, ); expect(data).toEqual({ firstname: 'John', lastname: 'Doe', }); }); it('invokes the pipeline several time if repeat_while_true is true', async () => { const data = await aggregator.runStepIf( { type: 'if', path: 'count', repeat_while_true: true, schema: { type: 'number', maximum: 2, }, pipeline: [ { type: 'op', func: 'add', destination: 'count', args: [ { path: 'count', }, { default: 1, }, ], }, ], }, { count: 0, }, ); expect(data).toEqual({ count: 3, }); }); it('stops the step after 100 iterations if not defined', async () => { const data = await aggregator.runStepIf( { type: 'if', path: 'count', repeat_while_true: true, schema: { type: 'number', }, pipeline: [ { type: 'op', func: 'add', destination: 'count', args: [ { path: 'count', }, { default: 1, }, ], }, ], }, { count: 0, }, ); expect(data).toEqual({ count: 100, }); }); it('stops the step after `max_iteration_count` iterations if defined', async () => { const data = await aggregator.runStepIf( { type: 'if', path: 'count', repeat_while_true: true, max_iteration_count: 2, schema: { type: 'number', }, pipeline: [ { type: 'op', func: 'add', destination: 'count', args: [ { path: 'count', }, { default: 1, }, ], }, ], }, { count: 0, }, ); expect(data).toEqual({ count: 2, }); }); }); describe('#runStepFilter', () => { it('throws an exception on uninitialized data', async () => { let error; try { const data = await aggregator.runStepFilter({ type: 'filter', path: 'items', destination: 'filtered', schema: { type: 'object', properties: { id: { type: 'number', enum: [1], }, }, }, }); } catch (err) { error = err; } expect(error).toEqual(Aggregator.ERROR_ITERATE_STEP_IS_NOT_ARRAY); }); it('filter an array based on a JSON Schema',