@getanthill/datastore
Version:
Event-Sourced Datastore
1,880 lines (1,626 loc) • 47.1 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',
});
});
});
/**
* 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',
});
});
});
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',
});
});
});
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',
},
});
});
});
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: {},
});
});
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'],
},
});
});
});
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',
},
});
});
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',
},
});
});
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',
},
});
});
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',
});
});
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',
},
],
});
});
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',
},
],
});
});
});
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',
},
],
});
});
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',
},
],
});
});
});
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',
},
});
});
});
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',
},
});
});
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',
},
});
});
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',
},
],
},
});
});
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',
});
});
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,
},
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,
},
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',
});
});
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,
},
});
});
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',
});
});
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',
});
});
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',
});
});
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: {},
});
});
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'],
},
});
});
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,
},
});
});
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,
},
});
});
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'],
},
});
});
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,
},
});
});
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,
},
});
});
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'],
},
});
});
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,
},
});
});
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,
},
});
});
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'],
},
});
});
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: [],
});
});
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 ⚠', () => {
/**
* GraphQL
*/
describe('⚠ #graphql', () => {
beforeEach(() => {
client.core._axios = {
request: jest.fn().mockImplementation(() => ({
data: {},
})),
};
});
afterEach(() => {
jest.restoreAllMocks();
});
it('queries graphql', async () => {
await client.query(`user`, {}, 'Operation');
expect(client.core._axios.request).toHaveBeenLastCalledWith({
method: 'post',
url: '/api/graphql',
data: {
query: `
query Operation($token: String!) {
viewerApiKey(apiKey: $token) {
user
}
}`,
operationName: 'Operation',
variables: {
token: 'token',
},
},
});
});
it('requests a mutation on graphql', async () => {
await client.mutation(`updateUser{}`);
expect(client.core._axios.request).toHaveBeenLastCalledWith({
method: 'post',
url: '/api/graphql',
data: {
query: `
mutation Op($token: String!) {
mutationViewerApiKey(apiKey: $token) {
updateUser{}
}
}`,
operationName: 'Op',
variables: {
token: 'token',
},
},
});
});
});
// 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).toBeCalledWith(
'http://localhost:3001/api/stream/all/entities/sse?pipeline=[]',
{
headers: {
authorization: 'token',
},
withCredentials: true,
},
);
});
it('calls the stream route for a given model', async () => {
const handler = jest.fn();
close = await client.stream(handler, 'users');
expect(client.streams.getEventSource).toBeCalledWith(
'http://localhost:3001/api/stream/users/entities/sse?pipeline=[]',
{
headers: {
authorization: 'token',
},
withCredentials: true,
},
);
});
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).toBeCalledWith(
'http://localhost:3001/api/stream/users/events/sse?pipeline=[]',
{
headers: {
authorization: 'token',
},
withCredentials: true,
},
);
});
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).toBeCalledWith(
'http://localhost:3001/api/stream/users/entities/sse?pipeline=[{"fullDocument.email":"john"}]',
{
headers: {
authorization: 'token',
},
withCredentials: true,
},
);
});
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 });
});
});
});
});