@getanthill/datastore
Version:
Event-Sourced Datastore
364 lines (313 loc) • 7.43 kB
text/typescript
import type { Models } from '.';
import type { Services } from '../typings';
interface Node {
[key: string]: string | number;
}
export type NodeI = Node & {
id?: string;
group?: number;
};
interface Edge {
[key: string]: string | number;
}
export type EdgeI = Edge & {
source?: string;
target?: string;
value?: number;
key?: string;
correlation_field?: string;
};
interface Graph {
[key: string]: NodeI[] | EdgeI[];
}
export type GraphI = Graph & {
nodes?: NodeI[];
edges?: EdgeI[];
};
interface Field {
[key: string]: string;
}
type DefaultFields = Field & {
id: string;
group: string;
nodes: string;
edges: string;
source: string;
target: string;
value: string;
key: string;
entity_id_suffix: string;
entity_type_suffix: string;
correlation_field: string;
};
const DEFAULT_FIELDS = {
id: 'id',
group: 'group',
nodes: 'nodes',
edges: 'edges',
source: 'source',
target: 'target',
value: 'value',
key: 'key',
entity_id_suffix: '_id',
entity_type_suffix: '_type',
correlation_field: 'correlation_field',
};
function addNode(fields: DefaultFields, id: string, group: number): NodeI {
return {
[fields.id]: id,
[fields.group]: group,
};
}
function addEdge(
fields: DefaultFields,
source: string,
target: string,
value: number,
key: string,
correlationField: string,
): EdgeI {
return {
[fields.source]: source,
[fields.target]: target,
[fields.value]: value,
[fields.key]: key,
[fields.correlation_field]: correlationField,
};
}
function getFieldsFromOptions(options: any) {
return {
...DEFAULT_FIELDS,
...options?.fields,
};
}
function addEdgesFromDiscovery(
models: Models,
graph: Graph,
modelName: string,
key: string,
modelNames = [],
options?: any,
) {
const fields = getFieldsFromOptions(options);
modelNames.forEach((l) => {
if (!models.hasModel(l)) {
return;
}
const linkedModel = models.getModel(l);
graph[fields.edges].push(
addEdge(fields, modelName, l, 1, key, linkedModel.getCorrelationField()),
);
});
}
function addEdgeWithoutLink(
models: Models,
graph: Graph,
modelName: string,
key: string,
options?: any,
) {
const fields = getFieldsFromOptions(options);
if (key.endsWith(fields.entity_id_suffix)) {
const target: string = key
.replace(fields.entity_id_suffix, 's')
.replace(/ss$/, 's');
if (!models.hasModel(target) || target === modelName) {
return;
}
graph[fields.edges].push(
addEdge(
fields,
modelName,
target,
1,
key,
models.getModel(target).getCorrelationField(),
),
);
}
}
function addEdgesFromEntityTypeLink(
models: Models,
graph: Graph,
modelName: string,
key: string,
modelNames: string[] = [],
options?: any,
) {
const fields = getFieldsFromOptions(options);
modelNames.forEach((l) =>
graph[fields.edges].push(
addEdge(
fields,
modelName,
l,
1,
key,
models.getModel(l).getCorrelationField(),
),
),
);
}
function addEdgesFromProperties(
models: Models,
graph: Graph,
modelName: string,
options: any,
) {
const fields = getFieldsFromOptions(options);
const model = models.getModel(modelName);
const schema = model.getOriginalSchema();
const properties = schema?.model?.properties ?? {};
const links = model.getModelConfig()?.links ?? [];
for (const key in properties) {
/* @ts-ignore */
const link = links[key];
const entityKey = key.replace(fields.entity_id_suffix, '');
const entityTypeKey = `${entityKey}${fields.entity_type_suffix}`;
/**
* Discovery process
*/
if (options.mustDiscover !== false && properties[entityTypeKey]) {
addEdgesFromDiscovery(
models,
graph,
modelName,
key,
properties[entityTypeKey].enum,
options,
);
continue;
}
/**
* group_id -> groups
* account_id -> accounts
*/
if (!link) {
addEdgeWithoutLink(models, graph, modelName, key, options);
continue;
}
if (properties[link]) {
addEdgesFromEntityTypeLink(
models,
graph,
modelName,
key,
properties[link].enum,
options,
);
continue;
}
if (!models.hasModel(link)) {
continue;
}
graph[fields.edges].push(
addEdge(
fields,
modelName,
link,
1,
key,
models.getModel(link).getCorrelationField(),
),
);
}
}
export default function getGraph(models: Models, options: any = {}): GraphI {
const fields = getFieldsFromOptions(options);
const graph: Graph = {
[fields.nodes]: [],
[fields.edges]: [],
};
for (const modelName of models.MODELS.keys()) {
if (models.isInternalModel(modelName) === true) {
continue;
}
graph[fields.nodes].push(
addNode(fields, modelName, graph[fields.nodes].length),
);
addEdgesFromProperties(models, graph, modelName, options);
}
return graph;
}
function getEntityId(modelName: string, correlationId: string): string {
return `${modelName}:${correlationId}`;
}
export async function getEntitiesFromGraph(
services: Services,
modelName: string,
query: any,
options: {
graph?: GraphI;
models?: string[];
withCorrelationFieldOnly?: boolean;
handler?: Function;
} = {},
entities: Map<string, any> = new Map(),
): Promise<Map<string, any>> {
if (
(Array.isArray(options.models) && !options.models.includes(modelName)) ||
!services.models
) {
return entities;
}
const graph: GraphI = options.graph ?? getGraph(services.models);
const Model = services.models.getModel(modelName);
const correlationField: string = Model.getCorrelationField();
const entityIds: Set<string> = new Set();
const cursor = await Model.find(
services.mongodb,
{
...query,
[Model.getIsArchivedProperty()]: {
$in: [null, false, true],
},
},
{
projection: {
_id: 0,
...(options.withCorrelationFieldOnly === true
? {
[correlationField]: 1,
}
: {}),
},
},
);
while (await cursor.hasNext()) {
let entity = await cursor.next();
const entityId = getEntityId(modelName, entity[correlationField]);
if (entities.has(entityId)) {
continue;
}
if ('handler' in options && options.handler) {
entity = await options.handler(services, Model, entity);
}
entityIds.add(entity[correlationField]);
entities.set(entityId, entity);
}
// Descendant nodes
const childrenModels = graph?.edges?.filter(
(edge) => edge.target === modelName,
);
for (const childModel of childrenModels!) {
const sourceModel: any = services.models.getModel(childModel.source!);
const link = sourceModel.getModelConfig()?.links?.[childModel.key!];
const childQuery: any = {
[childModel.key!]: {
$in: Array.from(entityIds.values()),
},
};
if (sourceModel.getSchema().model.properties[link]) {
childQuery[link] = childModel.target;
}
await getEntitiesFromGraph(
services,
childModel.source!,
childQuery,
{ ...options, graph },
entities,
);
}
return entities;
}