import { curry, is, last, mergeRightDeep, path } from '../fn.js' import { Stream } from '../stream.js' class Value { value constructor(value) { this.value = value } toString() { return this.value.toString() } valueOf() { return this.value?.valueOf() ?? this.value } equals(other) { if (other == null) { return false } const value = this.valueOf() return value === other || value === other.valueOf() } } export class Identifier extends Value { constructor(value) { super(value) } from(node) { return node[this.valueOf()] } pluck(node) { const key = this.valueOf() return { [key]: node[key] } } } export class Property extends Identifier { constructor(value) { super(value) } pluck(node) { return node[this.valueOf()] } } export class Name extends Identifier { constructor(value) { super(value) } } export class Label extends Identifier { constructor(value) { super(value) } } export class SelectedGraph extends Identifier { constructor(value) { super(value) } } export class Literal { constructor(value) { this.value = value } from(node) { return this.valueOf() } valueOf() { return this.value.valueOf() } } export const None = new Literal(null) export class Function { #fn constructor(name, fn, length = fn.length) { this.name = name this.#fn = fn this.length = length } #pop(stack) { const result = [] let n = Math.min(this.length, stack.length) while(n > 0) { result.push(stack.pop()) n-- } result.reverse() return result } apply(stack) { const args = this.#pop(stack) return this.#fn.apply(undefined, args) } } const Associativity = Object.freeze({ Left: 0, Right: 1 }) export class Operator extends Function { constructor(name, fn, priority, associativity = Associativity.Left, length = fn.length) { super(name, fn, length) this.precedence = priority this.associativity = associativity } compare(other) { if (other.precedence < this.precedence) { return 1 } else if (other.precedence === this.precedence) { if (this.associativity === Associativity.Left) { return 1 } else { return 0 } } else { return -1 } } } export const Is = new Operator('IS', (x, y) => typeof x === y, 2, Associativity.Right) export const Not = new Operator('NOT', x => !x, 2, Associativity.Right) export const And = new Operator('AND', (x, y) => x && y, 11) export const Or = new Operator('OR', (x, y) => x || y, 12) export const Xor = new Operator('XOR', (x, y) => x != y, 14) export const GreaterThan = new Operator('GT', (x, y) => x > y, 6) export const GreaterThanEqual = new Operator('GTE', (x, y) => x >= y, 6) export const LesserThan = new Operator('LT', (x, y) => x < y, 6) export const LesserThanEqual = new Operator('LTE', (x, y) => x <= y, 6) export const Equal = new Operator('EQ', (x, y) => x === y, 7) export class Filter { constructor(...args) { this.values = this.#shuntingYard(args) } #takeWhile(fn, stack) { const result = [] let val = last(stack) while (stack.length > 0 && fn(val)) { result.push(stack.pop()) val = last(stack) } return result } #takeUntil(fn, stack) { return this.#takeWhile(x => !fn(x), stack) } #shuntingYard(args) { const stream = new Stream(args) const output = [] const operators = [] const valueOf = curry((x, y) => y.valueOf() === x) const leftParen = valueOf('(') const comma = valueOf(',') const rightParen = valueOf(')') while (!stream.done()) { const next = stream.next() if (is(Function, next)) { if (is(Operator, next)) { const ops = this.#takeWhile(op => !leftParen(op) && next.compare(op) > 0, operators) output.push.apply(output, ops) } operators.push(next) } else if (comma(next)) { output.push.apply(output, this.#takeUntil(leftParen, operators)) } else if (leftParen(next)) { operators.push(next) } else if (rightParen(next)) { const ops = this.#takeUntil(leftParen, operators) output.push.apply(output, ops) if (!leftParen(operators.pop())) { throw new SyntaxError('mismatched parenthesis') } if (is(Function, last(operators))) { output.push(operators.pop()) } } else { output.push(next) } } const rest = this.#takeUntil(leftParen, operators) output.push.apply(output, rest) if (operators.length > 0) { throw new SyntaxError('mismatched parenthesis') } return output } passes(nodes) { return nodes.every(node => { const values = this.values.map(v => v.from ? v.from(node.values) : v) const stream = new Stream(values) const stack = [] while (!stream.done()) { const next = stream.next() if (is(Operator, next)) { const result = next.apply(stack) stack.push(result) } else { stack.push(next) } } return stack[0] }) } } export class Alias extends Identifier { constructor(value, alias) { super(value) this.alias = alias } pluck(node) { return { [this.alias.valueOf()]: this.value.from(node) } } } export class ObjectPath extends Identifier { constructor(value, ...path) { super(value) this.path = path } toString() { return [this.valueOf()].concat(this.path).join('.') } from(node) { return path(this.toString(), node) } pluck(node) { const values = this.path.reduce((acc, v) => ({ [v.valueOf()]: v.pluck(acc) }), node[this.valueOf()]) return { [this.valueOf()]: values } } equals(value) { if (is(ObjectPath, value)) { return value != null && this.path.length === value.path.length && super.equals(value) && this.path.every((x, i) => x === value.path[i]) } else { return super.equals(value) } } } class GraphObject { constructor(name, label, properties = []) { this.name = name this.label = label this.properties = properties } } export class Component extends GraphObject { constructor(name, label, properties) { super(name, label, properties) } } export class Node { constructor(components) { this.components = components } } export const Direction = Object.freeze({ Left: 0, Right: 1 }) export class Edge { constructor(components) { this.components = components } } export class DirectedEdge extends Edge { constructor(components, direction) { super(components) this.direction = direction } static fromEdge(edge, direction) { return new DirectedEdge(edge.components, direction) } } export class Relationship { constructor(left, edge, right) { const [from, to] = edge.direction === Direction.Right ? [left, right] : [right, left] this.from = from this.to = to this.edge = edge } } export class ReturnValues { constructor(...args) { this.values = args } from(values) { // TODO: pass with entries const returns = Object.keys(values).map(key => this.#pluck(values, key)) return Object.assign({}, ...returns) } #pluck(values, name) { return this.findName(name) .map(value => value.pluck(values)) .reduce(mergeRightDeep, {}) } find(fn) { return this.values.find(fn) } filter(fn) { return this.values.filter(fn) } findName(value) { return this.filter(x => x.equals(value)) } } export class KeyValuePair { constructor(key, value) { this.key = key this.value = value } } export class Match { constructor(value) { this.value = value } valueOf() { return this.value } } export class Query { constructor(use, matches, filters, returnValues) { this.use = use this.matches = matches this.filters = filters this.returnValues = returnValues } }