resolve values
This commit is contained in:
parent
efe4971ccc
commit
3f1686d084
4 changed files with 161 additions and 82 deletions
|
@ -43,6 +43,7 @@ export const converge = (fn, tfns) => curryN(
|
||||||
export const isArray = Array.isArray
|
export const isArray = Array.isArray
|
||||||
export const isNil = v => !v
|
export const isNil = v => !v
|
||||||
export const is = curry((type, value) => value instanceof type)
|
export const is = curry((type, value) => value instanceof type)
|
||||||
|
export const isString = value => is(String, value) || typeof value === 'string'
|
||||||
|
|
||||||
// branching
|
// branching
|
||||||
export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v))
|
export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v))
|
||||||
|
|
|
@ -1,21 +1,89 @@
|
||||||
export class Name {
|
import { is } from '../fn.js'
|
||||||
|
|
||||||
|
class Value {
|
||||||
|
#value
|
||||||
|
|
||||||
|
constructor(value) {
|
||||||
|
this.#value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.#value?.value ?? this.#value
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(other) {
|
||||||
|
if (other == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.value
|
||||||
|
return value === other
|
||||||
|
|| value === other.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Identifier extends Value {
|
||||||
|
constructor(value) {
|
||||||
|
super(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
constructor(value) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Label {
|
|
||||||
constructor(value) {
|
export class Alias extends Identifier {
|
||||||
this.value = value
|
constructor(value, alias) {
|
||||||
|
super(value)
|
||||||
|
this.alias = alias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelectedGraph {
|
export class ObjectPath extends Identifier {
|
||||||
constructor(identifier) {
|
constructor(value, ...path) {
|
||||||
this.identifier = identifier
|
super(value)
|
||||||
|
this.path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.path.reduce((acc, val) => acc + val, this.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
from(obj) {
|
||||||
|
return this.path.reduce((obj, key) => obj && obj[key], obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
equals(value) {
|
||||||
|
return value != null
|
||||||
|
&& is(ObjectPath, value)
|
||||||
|
&& this.path.length === value.path.length
|
||||||
|
&& super.equals(value)
|
||||||
|
&& this.path.every((x, i) => x === value.path[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class GraphObject {
|
class GraphObject {
|
||||||
constructor(name, label, properties = []) {
|
constructor(name, label, properties = []) {
|
||||||
this.name = name
|
this.name = name
|
||||||
|
@ -65,6 +133,14 @@ export class ReturnValues {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
this.values = args
|
this.values = args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find(fn) {
|
||||||
|
return this.values.find(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
includes(value) {
|
||||||
|
return this.find(value.equals.bind(value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeyValuePair {
|
export class KeyValuePair {
|
||||||
|
@ -74,36 +150,6 @@ export class KeyValuePair {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Literal {
|
|
||||||
constructor(value) {
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Identifier {
|
|
||||||
constructor(value) {
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
get literal() {
|
|
||||||
return this.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Alias {
|
|
||||||
constructor(value, alias) {
|
|
||||||
this.value = value
|
|
||||||
this.alias = alias
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ObjectPath {
|
|
||||||
constructor(value, ...path) {
|
|
||||||
this.value = value
|
|
||||||
this.path = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Query {
|
export class Query {
|
||||||
constructor(use, match, returnValues) {
|
constructor(use, match, returnValues) {
|
||||||
this.use = use
|
this.use = use
|
||||||
|
|
42
src/query.js
42
src/query.js
|
@ -1,7 +1,7 @@
|
||||||
import { defineQuery, hasComponent } from 'bitecs'
|
import { defineQuery, hasComponent } from 'bitecs'
|
||||||
import { query as q } from './query-parser/index.js'
|
import { query as q } from './query-parser/index.js'
|
||||||
import { parseAll } from './parser.js'
|
import { parseAll } from './parser.js'
|
||||||
import { curry, of, is, reduce, flip, concat, map, when, assoc, mergeLeft, isNil, path, prop, ifElse, pipe, always, assocPath, prepend, orDefault, applyTo, lens, over } from './fn.js'
|
import { curry, of, is, map, when, assoc, isNil, path, pipe, always, assocPath, prop } from './fn.js'
|
||||||
import { Relationship } from './query-parser/types.js'
|
import { Relationship } from './query-parser/types.js'
|
||||||
|
|
||||||
const prepareQuery = (query, component) => {
|
const prepareQuery = (query, component) => {
|
||||||
|
@ -14,9 +14,9 @@ const prepareQuery = (query, component) => {
|
||||||
|
|
||||||
return { from, to, edge, relationship, schema: query }
|
return { from, to, edge, relationship, schema: query }
|
||||||
} else {
|
} else {
|
||||||
const label = path('label.value.value', match)
|
const label = match.label.value
|
||||||
const name = path('name.value.value', match)
|
const name = match.name.value
|
||||||
const type = component[label]
|
const type = component[match.label.value]
|
||||||
|
|
||||||
return { name, label, type, relationship, query: defineQuery([type]), schema: query }
|
return { name, label, type, relationship, query: defineQuery([type]), schema: query }
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ const queryRelationship = (query, world) => {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const assembleQuery = (query, entities) => entities.map(entity => ([{ entity, query }]))
|
const assembleQuery = (query, entities) => map(entity => ([{ entity, query }]), entities)
|
||||||
|
|
||||||
const executeQuery = curry((query, world) => {
|
const executeQuery = curry((query, world) => {
|
||||||
if (!query.relationship) {
|
if (!query.relationship) {
|
||||||
|
@ -84,26 +84,39 @@ const executeQuery = curry((query, world) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const includes = curry((val, arr) => !isNil(val) && arr.includes(val))
|
const isTag = type => (
|
||||||
|
Object.getOwnPropertySymbols(type).find(
|
||||||
const maybeAssoc = curry((pred, key, value, obj) =>
|
(s) => s.description === 'tagStore'
|
||||||
when(pred, assoc(key, value), obj)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getValue = (type, entity) => {
|
||||||
|
if (isTag(type)) {
|
||||||
|
return {}
|
||||||
|
} else {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(type).map(([k, v]) => [k, v[entity]])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolveNode = curry((node, obj) => {
|
const resolveNode = curry((node, obj) => {
|
||||||
const name = path('query.schema.match.name.value.value', node)
|
const name = path('query.schema.match.name', node)
|
||||||
const returns = path('query.schema.returnValues.values', node).map(r => r.value)
|
const returns = path('query.schema.returnValues', node)
|
||||||
return maybeAssoc(always(includes(name, returns)), name, node.entity, obj)
|
const { entity, query: { type } } = node
|
||||||
|
const value = getValue(type, entity)
|
||||||
|
return returns.includes(name) ? { ...obj, [name.value]: value } : obj
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolveRelationship = edges => {
|
const resolveRelationship = edges => {
|
||||||
const { entity, query } = edges
|
const { entity, query } = edges
|
||||||
const Edge = query.edge.type
|
const Edge = query.edge.type
|
||||||
|
|
||||||
|
const to = query.to?.from ?? query.to
|
||||||
return pipe(
|
return pipe(
|
||||||
resolveNode({ entity: Edge.from[entity], query: query.from }),
|
resolveNode({ entity: Edge.from[entity], query: query.from }),
|
||||||
resolveNode({ entity, query: query.edge }),
|
resolveNode({ entity, query: query.edge }),
|
||||||
resolveNode({ entity: Edge.to[entity], query: query.to }),
|
resolveNode({ entity: Edge.to[entity], query: to }),
|
||||||
)({})
|
)({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +139,8 @@ export const query = curry((input, { world, component }) => {
|
||||||
const useWorld = world[use?.value ?? 'default']
|
const useWorld = world[use?.value ?? 'default']
|
||||||
const preparedQuery = prepareQuery(rest, component)
|
const preparedQuery = prepareQuery(rest, component)
|
||||||
const results = executeQuery(preparedQuery, useWorld)
|
const results = executeQuery(preparedQuery, useWorld)
|
||||||
return resolveReturns(results)
|
const returns = resolveReturns(results)
|
||||||
|
return returns
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,34 @@ import { beforeEach, describe, it } from 'node:test'
|
||||||
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
|
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
|
||||||
import assert from './assert.js'
|
import assert from './assert.js'
|
||||||
import { query } from '../src/query.js'
|
import { query } from '../src/query.js'
|
||||||
|
import { identity } from '../src/fn.js'
|
||||||
|
|
||||||
let engine = {}
|
let engine = {}
|
||||||
|
|
||||||
|
const create = (world, ...components) => {
|
||||||
|
const entity = addEntity(world)
|
||||||
|
components.forEach(cmp => addComponent(world, cmp, entity))
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
const relate = (world, a, type, ...b) => {
|
||||||
|
return b.map(v => {
|
||||||
|
const edge = addEntity(world)
|
||||||
|
addComponent(world, type, edge)
|
||||||
|
type.from[edge] = a
|
||||||
|
type.to[edge] = v
|
||||||
|
return edge
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
describe('query', () => {
|
describe('query', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const world = { default: createWorld() }
|
const world = { default: createWorld() }
|
||||||
const component = {
|
const component = {
|
||||||
|
Entity: identity,
|
||||||
Player: defineComponent(null, 10),
|
Player: defineComponent(null, 10),
|
||||||
NPC: defineComponent(null, 10),
|
NPC: defineComponent(null, 10),
|
||||||
|
Health: defineComponent({ current: Types.ui8, max: Types.ui8 }),
|
||||||
Knows: defineComponent({
|
Knows: defineComponent({
|
||||||
from: Types.eid,
|
from: Types.eid,
|
||||||
to: Types.eid
|
to: Types.eid
|
||||||
|
@ -19,56 +39,54 @@ describe('query', () => {
|
||||||
engine = { world, component }
|
engine = { world, component }
|
||||||
})
|
})
|
||||||
|
|
||||||
it('i just need a test runner', () => {
|
it('should execute basic queries', () => {
|
||||||
const w = engine.world.default
|
const world = engine.world.default
|
||||||
const { Player, NPC, Knows } = engine.component
|
const { Player, NPC, Knows } = engine.component
|
||||||
|
|
||||||
const create = (...components) => {
|
const player = create(world, Player) // 0
|
||||||
const eid = addEntity(w)
|
const a = create(world, NPC) // 1
|
||||||
components.forEach(cmp => addComponent(w, cmp, eid))
|
const b = create(world, NPC) // 2
|
||||||
return eid
|
const c = create(world, NPC) // 3
|
||||||
}
|
|
||||||
|
|
||||||
const relate = (a, type, ...b) => {
|
relate(world, player, Knows, a, c) // 4, 5
|
||||||
return b.map(v => {
|
relate(world, a, Knows, player, b, c) // 6, 7, 8
|
||||||
const edge = addEntity(w)
|
relate(world, b, Knows, a) // 9
|
||||||
addComponent(w, type, edge)
|
relate(world, c, Knows, player) // 10
|
||||||
type.from[edge] = a
|
|
||||||
type.to[edge] = v
|
|
||||||
return edge
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = create(Player) // 0
|
|
||||||
const a = create(NPC) // 1
|
|
||||||
const b = create(NPC) // 2
|
|
||||||
const c = create(NPC) // 3
|
|
||||||
|
|
||||||
relate(player, Knows, a, c) // 4, 5
|
|
||||||
relate(a, Knows, player, b, c) // 6, 7, 8
|
|
||||||
relate(b, Knows, a) // 9
|
|
||||||
relate(c, Knows, player) // 10
|
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
query('MATCH (player:Player) RETURN player', engine).unwrap(),
|
query('MATCH (player:Player) RETURN player', engine).unwrap(),
|
||||||
[{ player: 0 }]
|
[{ player: {} }]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(),
|
query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(),
|
||||||
[
|
[
|
||||||
{ player: 0, e1: 4, a: 1 },
|
{ player: {}, e1: { from: 0, to: 1 }, a: {} },
|
||||||
{ player: 0, e1: 5, a: 3 }
|
{ player: {}, e1: { from: 0, to: 3 }, a: {} }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(),
|
query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(),
|
||||||
[
|
[
|
||||||
{ player: 0, e1: 4, a: 1, e2: 7, b: 2 },
|
{ player: {}, e1: { from: 0, to: 1 }, a: {}, e2: { from: 1, to: 2 }, b: {} },
|
||||||
{ player: 0, e1: 4, a: 1, e2: 8, b: 3 }
|
{ player: {}, e1: { from: 0, to: 1 }, a: {}, e2: { from: 1, to: 3 }, b: {} }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should match multiple components', () => {
|
||||||
|
const world = engine.world.default
|
||||||
|
const { Player, Health } = engine.component
|
||||||
|
const player = create(world, Player, Health)
|
||||||
|
|
||||||
|
Health.max[player] = 50
|
||||||
|
Health.current[player] = 25
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
query('MATCH (h:Health) return h.max', engine).unwrap(),
|
||||||
|
[{ h: { current: 25, max: 50 } }]
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue