@getanthill/datastore
Version:
Event-Sourced Datastore
1,551 lines (1,304 loc) • 44.2 kB
text/typescript
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 });
});
});
});
});