@getanthill/datastore
Version:
Event-Sourced Datastore
1,061 lines (968 loc) • 24.6 kB
text/typescript
import type { Services } from '../typings';
import setup from '../../test/setup';
import { init } from '.';
describe('models', () => {
let services: Services;
const DEFAULT_MODEL = {
is_enabled: true,
db: 'datastore',
retry_duration: 0,
indexes: [],
schema: {
model: {
properties: {
is_readonly: {
type: 'boolean',
},
},
},
},
};
beforeAll(async () => {
const app = await setup.build();
services = app.services;
});
afterEach(async () => {
jest.restoreAllMocks();
});
afterAll(async () => {
await setup.teardownDb(services.mongodb);
});
describe('#getGraph', () => {
it('returns default graph', () => {
const models = init(
{
models: [],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [],
edges: [],
});
});
it('keeps graph in instance cache', () => {
const models = init(
{
models: [],
},
services,
);
const graph = models.getGraph();
expect(models.GRAPH).not.toEqual(null);
expect(graph).toEqual(models.GRAPH);
});
it('returns default graph from instance cache', () => {
const models = init(
{
models: [],
},
services,
);
models.getGraph();
const graph = models.getGraph();
expect(graph).toEqual(models.GRAPH);
});
it('returns graph with custom properties', () => {
const models = init(
{
models: [],
},
services,
);
const graph = models.getGraph({
fields: {
edges: 'links',
},
});
expect(graph).toEqual({
nodes: [],
links: [],
});
});
it('returns one node per active model', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
],
edges: [],
});
});
it('filters links on `_id` without active model', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
account_id: {
type: 'string',
},
},
},
},
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
],
edges: [],
});
});
it('filters auto links on `_id`', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
user_id: {
type: 'string',
},
},
},
},
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
],
edges: [],
});
});
it('returns zero edge if no property is defined', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
schema: {},
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
],
edges: [],
});
});
it('returns one edge on fields ending with `_id`', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
account_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
],
edges: [
{
source: 'users',
target: 'accounts',
key: 'account_id',
value: 1,
correlation_field: 'account_id',
},
],
});
});
it('returns one edge on fields ending with `_id` but with an `s`', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
flexes_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'flexes',
correlation_field: 'flex_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'flexes',
},
],
edges: [
{
source: 'users',
target: 'flexes',
key: 'flexes_id',
value: 1,
correlation_field: 'flex_id',
},
],
});
});
it('returns one edge on fields listed in links', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
links: {
my_account_id: 'accounts',
},
schema: {
model: {
properties: {
my_account_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
],
edges: [
{
source: 'users',
target: 'accounts',
key: 'my_account_id',
value: 1,
correlation_field: 'account_id',
},
],
});
});
it('filters edges on unknown models', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
links: {
my_account_id: 'unknown',
},
schema: {
model: {
properties: {
my_account_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
],
edges: [],
});
});
it('returns one edge on fields listed in links via external enum', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
links: {
entity_id: 'entity_type',
},
schema: {
model: {
properties: {
entity_type: {
type: 'string',
enum: ['accounts', 'devices'],
},
entity_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
{
...DEFAULT_MODEL,
name: 'devices',
correlation_field: 'device_id',
},
],
},
services,
);
const graph = models.getGraph({
mustDiscover: false,
});
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
{
group: 2,
id: 'devices',
},
],
edges: [
{
source: 'users',
target: 'accounts',
key: 'entity_id',
value: 1,
correlation_field: 'account_id',
},
{
source: 'users',
target: 'devices',
key: 'entity_id',
value: 1,
correlation_field: 'device_id',
},
],
});
});
it('returns zero edge on fields listed in links via external enum but without enum defined', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
links: {
entity_id: 'entity_type',
},
schema: {
model: {
properties: {
entity_type: {
type: 'string',
},
entity_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
{
...DEFAULT_MODEL,
name: 'devices',
correlation_field: 'device_id',
},
],
},
services,
);
const graph = models.getGraph({
mustDiscover: false,
});
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
{
group: 2,
id: 'devices',
},
],
edges: [],
});
});
it('returns one edge based on correlation discovery', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
entity_type: {
type: 'string',
enum: ['accounts', 'devices'],
},
entity_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
{
...DEFAULT_MODEL,
name: 'devices',
correlation_field: 'device_id',
},
],
},
services,
);
const graph = models.getGraph();
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'accounts',
},
{
group: 2,
id: 'devices',
},
],
edges: [
{
source: 'users',
target: 'accounts',
key: 'entity_id',
value: 1,
correlation_field: 'account_id',
},
{
source: 'users',
target: 'devices',
key: 'entity_id',
value: 1,
correlation_field: 'device_id',
},
],
});
});
it('does not add edge on not found node', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
entity_type: {
type: 'string',
enum: ['accounts', 'devices'],
},
entity_id: {
type: 'string',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'devices',
correlation_field: 'device_id',
},
],
},
services,
);
const graph = models.getGraph({
mustDiscover: true,
});
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
{
group: 1,
id: 'devices',
},
],
edges: [
{
source: 'users',
target: 'devices',
key: 'entity_id',
value: 1,
correlation_field: 'device_id',
},
],
});
});
it('does not add edge on models without constraint on enum', () => {
const models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
schema: {
model: {
properties: {
entity_type: {
type: 'string',
},
entity_id: {
type: 'string',
},
},
},
},
},
],
},
services,
);
const graph = models.getGraph({
mustDiscover: true,
});
expect(graph).toEqual({
nodes: [
{
group: 0,
id: 'users',
},
],
edges: [],
});
});
});
describe('#setGraph', () => {
it('sets the instance graph', () => {
const models = init(
{
models: [],
},
services,
);
models.getGraph();
models.setGraph(null);
expect(models.GRAPH).toEqual(null);
});
});
describe('#getEntitiesFromGraph', () => {
let models;
beforeEach(() => {
models = init(
{
models: [
{
...DEFAULT_MODEL,
name: 'accounts',
correlation_field: 'account_id',
},
{
...DEFAULT_MODEL,
name: 'users',
correlation_field: 'user_id',
links: {
account_id: 'accounts',
},
schema: {
model: {
properties: {
firstname: {
type: 'string',
},
account_id: {
type: 'string',
},
is_readonly: {
type: 'boolean',
},
},
},
},
},
{
...DEFAULT_MODEL,
name: 'devices',
correlation_field: 'device_id',
links: {
entity_id: 'entity_type',
owner_id: 'accounts',
},
schema: {
model: {
properties: {
name: {
type: 'string',
},
entity_type: {
type: 'string',
enum: ['accounts', 'users'],
},
entity_id: {
type: 'string',
},
owner_id: {
type: 'string',
},
is_readonly: {
type: 'boolean',
},
},
},
},
},
],
},
services,
);
services.models = models;
});
it('returns only children entities', async () => {
const account = await models.factory('accounts').create({});
const user = await models.factory('users').create({
firstname: 'John',
account_id: account.state.account_id,
});
const entities = await models.getEntitiesFromGraph('users', {
user_id: user.state.user_id,
});
expect(Array.from(entities.values())).toEqual([user.state]);
});
it('returns the all children entities including nested levels', async () => {
const account = await models.factory('accounts').create({});
const user = await models
.factory('users')
.create({ firstname: 'John', account_id: account.state.account_id });
const userDevice = await models.factory('devices').create({
name: 'John device',
entity_type: 'users',
entity_id: user.state.user_id,
});
const entities = await models.getEntitiesFromGraph('accounts', {
account_id: account.state.account_id,
});
expect(Array.from(entities.values())).toEqual([
account.state,
user.state,
userDevice.state,
]);
});
it('returns only the correlation field if requested', async () => {
const account = await models.factory('accounts').create({});
const user = await models
.factory('users')
.create({ firstname: 'John', account_id: account.state.account_id });
const userDevice = await models.factory('devices').create({
name: 'John device',
entity_type: 'users',
entity_id: user.state.user_id,
});
const entities = await models.getEntitiesFromGraph(
'accounts',
{
account_id: account.state.account_id,
},
{
withCorrelationFieldOnly: true,
},
);
expect(Array.from(entities.values())).toEqual([
{ account_id: account.state.account_id },
{ user_id: user.state.user_id },
{ device_id: userDevice.state.device_id },
]);
});
it('returns required models', async () => {
const account = await models.factory('accounts').create({});
const user = await models
.factory('users')
.create({ firstname: 'John', account_id: account.state.account_id });
const userDevice = await models.factory('devices').create({
name: 'John device',
entity_type: 'users',
entity_id: user.state.user_id,
});
const entities = await models.getEntitiesFromGraph(
'accounts',
{
account_id: account.state.account_id,
},
{
models: ['accounts', 'users'],
},
);
expect(Array.from(entities.values())).toEqual([
account.state,
user.state,
]);
});
it('stops walking the graph on cyclic dependencies', async () => {
const account = await models.factory('accounts').create({});
const differentOwnerAccount = await models.factory('accounts').create({});
const user = await models
.factory('users')
.create({ firstname: 'John', account_id: account.state.account_id });
const userDevice = await models.factory('devices').create({
name: 'John device',
entity_type: 'users',
entity_id: user.state.user_id,
owner_id: account.state.account_id,
});
const userDevice2 = await models.factory('devices').create({
name: 'John device 2',
entity_type: 'users',
entity_id: user.state.user_id,
owner_id: differentOwnerAccount.state.account_id,
});
const entities = await models.getEntitiesFromGraph('accounts', {
account_id: account.state.account_id,
});
expect(Array.from(entities.values())).toEqual([
account.state,
user.state,
userDevice.state,
userDevice2.state,
]);
});
it('allows to perform an action on every visited entity', async () => {
const account = await models.factory('accounts').create({});
const user = await models
.factory('users')
.create({ firstname: 'John', account_id: account.state.account_id });
const userDevice = await models.factory('devices').create({
name: 'John device',
entity_type: 'users',
entity_id: user.state.user_id,
});
const handler = async (services, Model, entity) => {
const isReadonlyProperty: string = Model.getIsReadonlyProperty();
const e = new Model(services, entity[Model.getCorrelationField()]);
await e.update({
[isReadonlyProperty]: true,
});
return e.state;
};
const entities = await models.getEntitiesFromGraph(
'accounts',
{
account_id: account.state.account_id,
},
{
handler,
},
);
expect(Array.from(entities.values())).toMatchObject([
{
account_id: account.state.account_id,
version: 1,
is_readonly: true,
},
{
user_id: user.state.user_id,
version: 1,
is_readonly: true,
},
{
device_id: userDevice.state.device_id,
version: 1,
is_readonly: true,
},
]);
// Check the effective application
let error;
try {
await models.getEntitiesFromGraph(
'accounts',
{
account_id: account.state.account_id,
},
{
handler: async (services, Model, entity) => {
const e = new Model(
services,
entity[Model.getCorrelationField()],
);
await e.update({
test: 2,
});
return e.state;
},
},
);
} catch (err) {
error = err;
}
expect(error.message).toEqual('Entity is readonly');
});
});
});