import { FormatType, NodeDef, OperatorType, Universe, ValueSourceDef } from "../modeling"; import {request} from "../shared"; const _decoder = new TextDecoder(); interface PipeResponse { parseErrors?: { error: string }[]; error?: string; node: NodeDef; model: string; } export class Pipe { private _modelNode?: NodeDef; private _nodeType?: 'get' | 'put'; private _model?: string; private _object?: string; private _orders: { path: string, descending: boolean, }[] = []; private _filters: { path: string, operator: OperatorType, value: any, }[] = []; constructor(private definition: string, private backend = 'https://api.vyze.io') { } public model(model_id: string) { const newPipe = this.copy(); newPipe._model = model_id; return newPipe; } public object(obj: string) { const newPipe = this.copy(); newPipe._object = obj; return newPipe; } public filter(path: string, operator: OperatorType, value: any) { const newPipe = this.copy(); newPipe._filters.push({ 'path': path, 'operator': operator, 'value': value, }) return newPipe; } public equal(path: string, value: any) { return this.filter(path, 'eq', value); } public greaterThan(path: string, value: any) { return this.filter(path, 'gt', value); } public lessThan(path: string, value: any) { return this.filter(path, 'lt', value); } public order(path: string, asc = true) { const newPipe = JSON.parse(JSON.stringify(this)); newPipe._orders.append({ 'path': path, 'descending': !asc, }) return newPipe } public listNode(): NodeDef | undefined { if (!this._modelNode) { return; } let node: NodeDef = { 'type': 'list', 'list': { 'entry': this._modelNode, } }; for (const o of this._orders) { node = { 'type': 'sort', 'sort': { 'order': { 'source': this.pathSource(o['path']), 'descending': o['descending'], }, 'node': node, } }; } for (const f of this._filters) { node = { 'type': 'filter', 'filter': { 'filter': { 'source': this.pathSource(f['path']), 'operator': f['operator'], 'value': f['value'], }, 'node': node, } }; } node = { 'type': 'context', 'context': { 'context': { 'environment': { 'type': 'primitive', }, 'value': this._model, }, 'node': { 'type': 'specials', 'specials': { 'type': 'list', 'direct': true, 'indirect': true, 'node': node, } } } }; return node; } public objectNode(): NodeDef | undefined { if (!this._modelNode) { return; } if (this._object) { return { 'type': 'context', 'context': { 'context': { 'environment': { 'type': 'primitive', }, 'value': this._object, }, 'node': this._modelNode, } }; } else if (this._model) { return { 'type': 'context', 'context': { 'context': { 'environment': { 'type': 'primitive', }, 'value': this._model, }, 'node': { 'type': 'specials', 'specials': { 'type': 'primitive', 'direct': true, 'indirect': true, 'node': this._modelNode, } } } }; } else { return undefined; } } private pathSource(path: string): ValueSourceDef { if (!this._modelNode) { throw new Error(`Path source is missing model node`); } const paths = path.length > 0 ? path.split('.') : []; const {node: sourceNode, format} = cutMapNodes(paths, this._modelNode); return { 'type': 'node', 'format': format, 'node': sourceNode, } } public async loadPipe(universe: Universe) { if (this.definition.startsWith('$')) { const endpointName = this.definition.substring(1); const endpointNode = universe.getEndpoint(endpointName) if (!endpointNode) { throw new Error(`Endpoint not found: ${endpointName}`) } this._modelNode = endpointNode.node?.getDefinition(); this._nodeType = endpointNode.type; if (endpointNode.context?.value) { const value = endpointNode.context.value; if (typeof value === 'string') { this._model = value; } } if (!this._model && endpointNode.context?.environment.model) { this._model = endpointNode.context.environment.model.getIdentifier(); } } else { const req = { 'universe': universe.definition, 'pipe': this.definition, } const response = await request({ method: 'POST', url: `${this.backend}/pipe`, body: JSON.stringify(req), }); const bodyResp = response.body as ArrayBuffer | undefined; if (!bodyResp) { throw Error(`Empty response, code: ${response.status}`); } const textResp = _decoder.decode(bodyResp as ArrayBuffer); const pipeResp: PipeResponse | undefined = JSON.parse(textResp); if (!pipeResp) { throw Error(`Failed parsing ${textResp}`); } if (pipeResp.parseErrors && pipeResp.parseErrors.length > 0) { throw Error(pipeResp.parseErrors[0].error); } if (pipeResp.error) { throw Error(pipeResp.error); } this._modelNode = pipeResp.node; this._model = pipeResp.model; } } public copy(): Pipe { const pipeCpy = new Pipe(this.definition, this.backend); if (this._model) { pipeCpy._model = JSON.parse(JSON.stringify(this._model)); } if (this._object) { pipeCpy._object = JSON.parse(JSON.stringify(this._object)); } pipeCpy._orders = JSON.parse(JSON.stringify(this._orders)); pipeCpy._filters = JSON.parse(JSON.stringify(this._filters)); return pipeCpy; } } export function cutMapNodes(path: string[], iterNode: NodeDef): { node: NodeDef, format: FormatType } { const iterType = iterNode.type; if (iterType == 'map' && iterNode.map) { if (path.length == 0) { throw new Error('Path leads to a map. Add a field by attaching it with a dot (.)'); } for (const e of iterNode.map.entries) { if (e.name == path[0]) { return cutMapNodes(path.slice(1), e.node); } } throw new Error(`Entry not found: {path[0]}`); } else if (iterType == 'value' && iterNode.value) { if (path.length != 0) { throw new Error(`Path exceeds the node structure. Leftover path: ...${path.join('.')}`); } return { node: JSON.parse(JSON.stringify(iterNode)), format: iterNode.value.format, }; } else if (iterType == 'relation' && iterNode.relation) { const {node: returnNode, format} = cutMapNodes(path, iterNode.relation.node); iterNode = JSON.parse(JSON.stringify(iterNode)); iterNode.relation!.node = returnNode; return {node: iterNode, format}; } else if (iterType == 'specials' && iterNode.specials) { const {node: returnNode, format} = cutMapNodes(path, iterNode.specials.node); iterNode = JSON.parse(JSON.stringify(iterNode)); iterNode.specials!.node = returnNode; return {node: iterNode, format}; } throw new Error(`Unsupported node type: ${iterType}`); }