import {Id} from '../shared'; import {Model} from './model'; import {Universe} from "./universe"; import {Interface} from "./interface"; import {load} from "js-yaml"; export type StringFormatType = 'string' | 'hex' | 'base64'; export type JSONByteFormatType = 'hex' | 'base64'; export type NumberFormatType = 'integer' | 'float'; export type BooleanFormatType = 'boolean'; export type JSONFormatType = StringFormatType | JSONByteFormatType | NumberFormatType | BooleanFormatType; export type FormatType = StringFormatType | JSONByteFormatType | NumberFormatType | BooleanFormatType | 'raw'; export type NodeType = 'endpoint' | 'reference' | 'context' | 'relation' | 'specials' | 'value' | 'instance' | 'filter' | 'sort' | 'slice' | 'aggregate' | 'list' | 'map'; export type EnvironmentType = 'primitive' | 'list' | 'keyed_list'; export type SourceType = 'node' | 'key'; export type AggregateType = 'count' | 'minimum' | 'maximum' | 'sum' | 'average'; export type FieldType = 'id' | 'name' | 'created' | 'user' | 'size' | 'data'; export type EndpointType = 'get'| 'put'; export interface NodeDef { [_: string]: any; type: NodeType; endpoint?: EndpointNodeDef; reference?: ReferenceNodeDef; context?: ContextNodeDef; relation?: RelationNodeDef; specials?: SpecialsNodeDef; value?: ValueNodeDef; instance?: InstanceNodeDef; filter?: FilterNodeDef; sort?: SortNodeDef; slice?: SliceNodeDef; aggregate?: AggregateNodeDef; list?: ListNodeDef; map?: MapNodeDef; } export interface EnvironmentDef { type: EnvironmentType; model?: string; } export interface ContextDef { environment: EnvironmentDef; value: string | string[] | Id | Id[] | undefined; } export interface Context { environment: Environment; value: string | string[] | Id | Id[] | undefined; } export interface ValueSourceDef { type: SourceType; format: FormatType; node?: NodeDef; } export interface ValueSource { type: SourceType; format: FormatType; node?: Node; } export interface Parameter { name: string; description: string; interface: Interface; } export interface EndpointNodeDef { [_: string]: any; id?: Id; type: EndpointType; name: string; node: NodeDef; context: ContextDef; interface?: Interface; parameters?: Parameter[]; } export interface ReferenceNodeDef { name: string; } export interface ContextNodeDef { context: ContextDef; node: NodeDef; } export interface RelationNodeDef { type: EnvironmentType; relation: string; reverse: boolean; node: NodeDef; } export interface SpecialsNodeDef { type: EnvironmentType; direct: boolean; indirect: boolean; node: NodeDef; } export interface ValueNodeDef { field: FieldType; format: FormatType; } export interface InstanceSwitchDef { type: string; value: any; } export interface InstanceNodeDef { format: FormatType; switches: InstanceSwitchDef[]; } export interface ListNodeDef { entry: NodeDef; keyFormat?: FormatType; keyName?: string; valueName?: string; } export interface FilterNodeDef { filter: { source: ValueSourceDef; operator: OperatorType; value: any; }; node: NodeDef; } export interface SortNodeDef { order: { source: ValueSourceDef; descending: boolean; }; node: NodeDef; } export interface SliceNodeDef { offset?: number; limit?: number; node: NodeDef; } export interface AggregateNodeDef { source: ValueSourceDef; function: AggregateType; } export interface MapNodeEntryDef { name: string; node: NodeDef; } export interface MapNodeDef { entries: MapNodeEntryDef[]; } export interface Environment { type: EnvironmentType; model?: Model; } export abstract class Node { parent?: Node; context?: Context; public abstract getType(): NodeType; public abstract getDefinition(): NodeDef | undefined; public abstract resolveDefinition(universe: Universe): NodeDef | undefined; } export class EndpointNode extends Node { type?: EndpointType; id?: Id; name?: string; node?: Node; parameters?: Parameter[]; interface?: Interface; public getType(): NodeType { return 'endpoint'; } public getDefinition(): NodeDef | undefined { if (!this.type || !this.node || !this.name || !this.context?.environment?.model) { return undefined; } const nodeDef = this.node.getDefinition(); if (!nodeDef) { return undefined; } const def: NodeDef & {endpoint: EndpointNodeDef} = { type: 'endpoint', endpoint: { id: this.id, type: this.type, node: nodeDef, name: this.name, context: { environment: { type: 'primitive', model: this.context.environment.model.name, }, value: this.context?.value, }, interface: this.interface, } }; if (this.parameters) { def.endpoint.parameters = this.parameters; } return def; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node) { return undefined; } return this.node.resolveDefinition(universe); } /** @internal */ public load(def: EndpointNodeDef, universe: Universe) { const node = loadEndpointNode(def, universe); this.type = node.type; this.id = node.id; this.name = node.name; this.node = node.node; this.context = node.context ? { environment: { type: node.context.environment.type, model: def.context.environment.model ? universe.getModel(def.context.environment.model) : undefined, }, value: def.context.value, } : undefined; this.parameters = node.parameters; this.interface = node.interface; } } /** * A node representing a reference to another node. */ export class ReferenceNode extends Node { node?: EndpointNode; public getType(): NodeType { return 'reference'; } public getDefinition(): NodeDef | undefined { if (!this.node?.name) { return undefined; } return { type: 'reference', reference: { name: this.node.name, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node || !this.node.name) { return undefined; } const referenceNode = universe.endpoints.get(this.node.name); if (!referenceNode) { return undefined; } return referenceNode.resolveDefinition(universe); } } export type OperatorType = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le'; export interface FilterEntryDef { source: ValueSource; operator: OperatorType; value: any; } export interface OrderEntryDef { source: ValueSource; descending: boolean; } /** * A node manipulating the context for its child node. */ export class ContextNode extends Node { node?: Node; public getType(): NodeType { return 'context'; } public getDefinition(): NodeDef | undefined { if (!this.node || !this.context) { return undefined; } const nodeDef = this.node.getDefinition(); if (!nodeDef) { return undefined; } return { type: 'context', context: { node: nodeDef, context: { environment: { type: this.context.environment.type, model: this.context.environment.model?.name, }, value: this.context.value, }, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node || !this.context) { return undefined; } const nodeDef = this.node.resolveDefinition(universe); if (!nodeDef) { return undefined; } let value: any = this.context.value; if (typeof value === 'string') { value = universe.getModel(value)?.object; } return { type: 'context', context: { node: nodeDef, context: { environment: { type: this.context.environment.type, model: this.context.environment.model?.object, }, value, }, } }; } } export class RelationNode extends Node { type?: EnvironmentType; relation?: string; reverse?: boolean; node?: Node; public getType(): NodeType { return 'relation'; } public getDefinition(): NodeDef | undefined { if (!this.node || !this.type || typeof this.reverse === 'undefined' || !this.relation) { return undefined; } const nodeDef = this.node.getDefinition(); if (!nodeDef) { return undefined; } return { type: 'relation', relation: { type: this.type, relation: this.relation, reverse: this.reverse, node: nodeDef, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node || !this.type || typeof this.reverse === 'undefined' || !this.relation) { return undefined; } const nodeDef = this.node.resolveDefinition(universe); if (!nodeDef) { return undefined; } const relationId = universe.getModel(this.relation)?.object; if (!relationId) { return undefined; } return { type: 'relation', relation: { type: this.type, relation: relationId, reverse: this.reverse, node: nodeDef, } }; } } export class SpecialsNode extends Node { type?: EnvironmentType; direct? = false; indirect? = false; node?: Node; public getType(): NodeType { return 'specials'; } public getDefinition(): NodeDef | undefined { if (!this.node || !this.type || typeof this.direct === 'undefined' || typeof this.indirect === 'undefined') { return undefined; } const nodeDef = this.node.getDefinition(); if (!nodeDef) { return undefined; } return { type: 'specials', specials: { type: this.type, direct: this.direct, indirect: this.indirect, node: nodeDef, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node || !this.type || typeof this.direct === 'undefined' || typeof this.indirect === 'undefined') { return undefined; } const nodeDef = this.node.resolveDefinition(universe); if (!nodeDef) { return undefined; } return { type: 'specials', specials: { type: this.type, direct: this.direct, indirect: this.indirect, node: nodeDef, } }; } } /** * A node representing a value derived from the current environment. */ export class ValueNode extends Node { field?: FieldType; format?: FormatType; public getType(): NodeType { return 'value'; } public getDefinition(): NodeDef | undefined { if (!this.field || !this.format) { return undefined; } return { type: 'value', value: { field: this.field, format: this.format, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.field || !this.format) { return undefined; } return { type: 'value', value: { field: this.field, format: this.format, } }; } } export class InstanceNode extends Node { format?: FormatType; switches?: InstanceSwitchDef[]; public getType(): NodeType { return 'instance'; } public getDefinition(): NodeDef | undefined { if (!this.format || !this.switches) { return undefined; } return { type: 'instance', instance: { format: this.format, switches: this.switches, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.format || !this.switches) { return undefined; } const switches: InstanceSwitchDef[] = []; for (const s of this.switches) { const typeId = universe.resolve(s.type); if (!typeId) { return undefined; } switches.push({ type: typeId, value: s.value, }) } return { type: 'instance', instance: { format: this.format, switches: switches, } }; } } export class ListNode extends Node { entry?: Node; keyFormat?: FormatType; keyName?: string; valueName?: string; public getType(): NodeType { return 'list'; } public getDefinition(): NodeDef | undefined { if (!this.entry) { return undefined; } const entryDef = this.entry.getDefinition(); if (!entryDef) { return undefined; } return { type: 'list', list: { entry: entryDef, keyFormat: this.keyFormat, keyName: this.keyName, valueName: this.valueName, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.entry) { return undefined; } const entryDef = this.entry.resolveDefinition(universe); if (!entryDef) { return undefined; } return { type: 'list', list: { entry: entryDef, keyFormat: this.keyFormat, keyName: this.keyName, valueName: this.valueName, } }; } } export class FilterNode extends Node { filter?: FilterEntryDef; node?: Node; public getType(): NodeType { return 'filter'; } public getDefinition(): NodeDef | undefined { if (!this.filter || !this.node) { return undefined; } const childNode = this.node.getDefinition(); if (!childNode) { return undefined; } const source: ValueSourceDef = { type: this.filter.source.type, format: this.filter.source.format, } if (this.filter.source.node) { source.node = this.filter.source.node.getDefinition(); } return { type: 'filter', filter: { filter: { source, operator: this.filter.operator, value: this.filter.value, }, node: childNode, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.filter || !this.node) { return undefined; } const childNode = this.node.resolveDefinition(universe); if (!childNode) { return undefined; } const source: ValueSourceDef = { type: this.filter.source.type, format: this.filter.source.format, } if (this.filter.source.node) { source.node = this.filter.source.node.resolveDefinition(universe); } return { type: 'filter', filter: { filter: { source, operator: this.filter.operator, value: this.filter.value, }, node: childNode, } }; } } export class SortNode extends Node { order?: OrderEntryDef; node?: Node; public getType(): NodeType { return 'sort'; } public getDefinition(): NodeDef | undefined { if (!this.order || !this.node) { return undefined; } const childNode = this.node.getDefinition(); if (!childNode) { return undefined; } const source: ValueSourceDef = { type: this.order.source.type, format: this.order.source.format, } if (this.order.source.node) { source.node = this.order.source.node.getDefinition(); } return { type: 'sort', sort: { order: { source, descending: this.order.descending, }, node: childNode, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.order || !this.node) { return undefined; } const childNode = this.node.resolveDefinition(universe); if (!childNode) { return undefined; } const source: ValueSourceDef = { type: this.order.source.type, format: this.order.source.format, } if (this.order.source.node) { source.node = this.order.source.node.resolveDefinition(universe); } return { type: 'sort', sort: { order: { source, descending: this.order.descending, }, node: childNode, } }; } } export class SliceNode extends Node { offset?: number; limit?: number; node?: Node; public getType(): NodeType { return 'slice'; } public getDefinition(): NodeDef | undefined { if (!this.node) { return undefined; } const childNode = this.node.getDefinition(); if (!childNode) { return undefined; } return { type: 'slice', slice: { offset: this.offset, limit: this.limit, node: childNode, } }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.node) { return undefined; } const childNode = this.node.resolveDefinition(universe); if (!childNode) { return undefined; } return { type: 'slice', slice: { offset: this.offset, limit: this.limit, node: childNode, } }; } } export class AggregateNode extends Node { source?: ValueSource; function?: AggregateType; public getType(): NodeType { return 'aggregate'; } public getDefinition(): NodeDef | undefined { if (!this.source || !this.function) { return undefined; } const source: ValueSourceDef = { type: this.source.type, format: this.source.format, } if (this.source.node) { source.node = this.source.node.getDefinition(); } return { type: 'aggregate', aggregate: { source, function: this.function, } } } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.source || !this.function) { return undefined; } const source: ValueSourceDef = { type: this.source.type, format: this.source.format, } if (this.source.node) { source.node = this.source.node.resolveDefinition(universe); } return { type: 'aggregate', aggregate: { source, function: this.function, } } } } export interface MapNodeEntry { name: string; node: Node; } export class MapNode extends Node { entries?: MapNodeEntry[]; public getType(): NodeType { return 'map'; } public getDefinition(): NodeDef | undefined { if (!this.entries) { return undefined; } const entries: MapNodeEntryDef[] = []; for (const entry of this.entries) { const entryDef = entry.node.getDefinition(); if (!entryDef) { continue; } entries.push({name: entry.name, node: entryDef}); } return { type: 'map', map: { entries, }, }; } public resolveDefinition(universe: Universe): NodeDef | undefined { if (!this.entries) { return undefined; } const entries: MapNodeEntryDef[] = []; for (const entry of this.entries) { const entryDef = entry.node.resolveDefinition(universe); if (!entryDef) { continue; } entries.push({name: entry.name, node: entryDef}); } return { type: 'map', map: { entries, }, }; } } export function loadNode(nodeDef: NodeDef, universe: Universe): Node { switch (nodeDef.type) { case 'endpoint': if (!nodeDef.endpoint) { throw new Error('expected endpoint definition'); } return loadEndpointNode(nodeDef.endpoint, universe); case 'reference': if (!nodeDef.reference) { throw new Error('expected reference definition'); } return loadReferenceNode(nodeDef.reference, universe); case 'context': if (!nodeDef.context) { throw new Error('expected context definition'); } return loadContextNode(nodeDef.context, universe); case 'relation': if (!nodeDef.relation) { throw new Error('expected relation definition'); } return loadRelationNode(nodeDef.relation, universe); case 'specials': if (!nodeDef.specials) { throw new Error('expected specials definition'); } return loadSpecialsNode(nodeDef.specials, universe); case 'value': if (!nodeDef.value) { throw new Error('expected value definition'); } return loadValueNode(nodeDef.value, universe); case 'instance': if (!nodeDef.instance) { throw new Error('expected instance definition'); } return loadInstanceNode(nodeDef.instance, universe); case 'filter': if (!nodeDef.filter) { throw new Error('expected filter definition'); } return loadFilterNode(nodeDef.filter, universe); case 'sort': if (!nodeDef.sort) { throw new Error('expected sort definition'); } return loadSortNode(nodeDef.sort, universe); case 'slice': if (!nodeDef.slice) { throw new Error('expected slice definition'); } return loadSliceNode(nodeDef.slice, universe); case 'aggregate': if (!nodeDef.aggregate) { throw new Error('expected aggregate definition'); } return loadAggregateNode(nodeDef.aggregate, universe); case 'list': if (!nodeDef.list) { throw new Error('expected list definition'); } return loadListNode(nodeDef.list, universe); case 'map': if (!nodeDef.map) { throw new Error('expected map definition'); } return loadMapNode(nodeDef.map, universe); default: throw new Error(`invalid node type '${nodeDef.type}'`); } } export function loadEndpointNode(endpointNodeDef: EndpointNodeDef, universe: Universe): EndpointNode { const node = new EndpointNode(); node.id = endpointNodeDef.id; node.type = endpointNodeDef.type; node.name = endpointNodeDef.name; node.context = { environment: { type: endpointNodeDef.context?.environment.type, model: endpointNodeDef.context.environment.model ? universe.getModel(endpointNodeDef.context.environment.model) : undefined, }, value: endpointNodeDef.context.value, }; node.parameters = endpointNodeDef.parameters; node.interface = endpointNodeDef.interface; node.node = loadNode(endpointNodeDef.node, universe); return node; } function loadReferenceNode(referenceNodeDef: ReferenceNodeDef, universe: Universe): ReferenceNode { const node = new ReferenceNode(); node.node = universe.endpoints.get(referenceNodeDef.name); return node; } function loadContextNode(contextNodeDef: ContextNodeDef, universe: Universe): ContextNode { const node = new ContextNode(); node.context = { environment: { type: contextNodeDef.context.environment.type, model: contextNodeDef.context.environment.model ? universe.getModel(contextNodeDef.context.environment.model) : undefined, }, value: contextNodeDef.context.value, }; node.node = loadNode(contextNodeDef.node, universe); return node; } function loadRelationNode(relationDef: RelationNodeDef, universe: Universe): RelationNode { const node = new RelationNode(); node.type = relationDef.type; node.relation = relationDef.relation; node.reverse = relationDef.reverse; node.node = loadNode(relationDef.node, universe); return node; } function loadSpecialsNode(specialsDef: SpecialsNodeDef, universe: Universe): SpecialsNode { const node = new SpecialsNode(); node.type = specialsDef.type; node.direct = specialsDef.direct; node.indirect = specialsDef.indirect; node.node = loadNode(specialsDef.node, universe); return node; } function loadValueNode(valueNodeDef: ValueNodeDef, universe: Universe): ValueNode { const node = new ValueNode(); node.field = valueNodeDef.field; node.format = valueNodeDef.format; return node; } function loadInstanceNode(instanceNodeDef: InstanceNodeDef, universe: Universe): InstanceNode { const node = new InstanceNode(); node.format = instanceNodeDef.format; node.switches = instanceNodeDef.switches; return node; } function loadFilterNode(filterNodeDef: FilterNodeDef, universe: Universe): FilterNode { const node = new FilterNode(); node.node = loadNode(filterNodeDef.node, universe); node.filter = { source: { type: filterNodeDef.filter.source.type, format: filterNodeDef.filter.source.format, node: filterNodeDef.filter.source.node ? loadNode(filterNodeDef.filter.source.node, universe) : undefined, }, operator: filterNodeDef.filter.operator, value: filterNodeDef.filter.value, }; return node; } function loadSortNode(sortNodeDef: SortNodeDef, universe: Universe): SortNode { const node = new SortNode(); node.node = loadNode(sortNodeDef.node, universe); node.order = { source: { type: sortNodeDef.order.source.type, format: sortNodeDef.order.source.format, node: sortNodeDef.order.source.node ? loadNode(sortNodeDef.order.source.node, universe) : undefined, }, descending: sortNodeDef.order.descending, }; return node; } function loadSliceNode(sliceNodeDef: SliceNodeDef, universe: Universe): SliceNode { const node = new SliceNode(); node.node = loadNode(sliceNodeDef.node, universe); node.offset = sliceNodeDef.offset; node.limit = sliceNodeDef.limit; return node; } function loadAggregateNode(aggNodeDef: AggregateNodeDef, universe: Universe): AggregateNode { const node = new AggregateNode(); node.source = { type: aggNodeDef.source.type, format: aggNodeDef.source.format, node: aggNodeDef.source.node ? loadNode(aggNodeDef.source.node, universe) : undefined, }; node.function = aggNodeDef.function; return node; } function loadListNode(listNodeDef: ListNodeDef, universe: Universe): ListNode { const node = new ListNode(); node.entry = loadNode(listNodeDef.entry, universe); node.keyFormat = listNodeDef.keyFormat; node.keyName = listNodeDef.keyName; node.valueName = listNodeDef.valueName; return node; } function loadMapNode(mapNodeDef: MapNodeDef, universe: Universe): MapNode { const node = new MapNode(); const entries: MapNodeEntry[] = []; for (const entry of mapNodeDef.entries) { entries.push({name: entry.name, node: loadNode(entry.node, universe)}); } node.entries = entries; return node; }