UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

364 lines (313 loc) 7.43 kB
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; }