Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions resumable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## "Computed subtree"

```
graffy.use('computed', computed({
cachePath: 'cache',
transform: (input) => ({ ... }),
collection: ['users', { }],
projection: []
}))




```

```gql
{
deals(by:'amount$desc', first: 3) {
name
related: { name }
dealContact(first: 10) {
name
}
}
}
```

State of the watch:
- watch watermark
- impliedEnd of the deals query
- paths of each related (3) <- not required if using a join
- paths of each dealContact (3) <- not required if using a join
- impliedEnd of each dealContact query (3)


Use a resume ID option to allow different providers to store this information.


## Filter operators

We will support the following
eq
ne
lt
lte
gt
gte
in
nin

ct // For finding an element in an array.
nct

all
any
not
4 changes: 4 additions & 0 deletions src/common/coding/struct.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export function encode(value) {

const nextKey = new WeakMap();

export function isEncodedKey(key) {
return key[0] === '\0';
}

export function decode(key) {
let i = 0;
const buffer = decodeB64(key, 0);
Expand Down
5 changes: 5 additions & 0 deletions src/docdb/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Graffy DocDB

Graffy module providing a persistent storage using Postgres. This module uses JSONB columns to store the data.

See [Graffy documentation](https://aravindet.github.io/graffy/) for more.
64 changes: 64 additions & 0 deletions src/docdb/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { merge, slice, setVersion, makeWatcher } from '@graffy/common';
// import { debug } from '@graffy/testing';

function cowalk(trees, visit, prefix = []) {
if (!trees.length) return;

let cKey = '';
const cPos = new Array(trees.length).fill(0);

while (true) {
let keyExists = false;

// From each tree, get the node that matches the ccurrent position
const nodes = cPos.map((pos, i) => {
const { key, end } = trees[i][pos];
if (key === cKey || (key <= cKey && end >= cKey)) {
keyExists = true;
return trees[i][pos];
}
});

if (keyExists && visit(prefix.concat(cKey), ...nodes)) {
// Descend into child nodes if the visitor returns true.
cowalk(
nodes.map(({ children } = {}) => children || []),
visit,
prefix.concat(cKey),
);
}

if (cKey === '\uffff') return;

// From all trees, find the earliest unvisited key to visit.
cKey = cPos.reduce((min, pos, i) => {
if (pos + 1 >= trees[i].length) return min;
const { key } = trees[i][pos + 1];
return key < min ? key : min;
}, '\uffff');

// Advance pos for those trees that have this cKey (this is a separate)
// step as the same key might exist in multiple trees, and we have to
// advance all of them.
}
}

export default function () {
const linkTrees = [{ key: 'user', children: [{ key: '', end: '\uffff' }] }];

return (store) => {
const watcher = makeWatcher();

store.on('read', [], async (query) => {
return setVersion(slice(state, query).known, Date.now());
});

store.on('watch', [], () => watcher.watch(undefined));

store.on('write', [], async (change) => {
merge(state, change);
watcher.write(change);
return change;
});
};
}
5 changes: 5 additions & 0 deletions src/docdb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@graffy/docdb",
"version": "1.0.0",
"type": "module"
}
37 changes: 37 additions & 0 deletions src/docdb/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Graffy from '@graffy/core';
import DocDB from './index.js';

describe('final', () => {
let store;

beforeEach(() => {
store = new Graffy();
store.use(
DocDB({
collections: ['user', 'post'],
links: [{ from: 'post', link: 'author', to: 'user', back: 'posts' }],
}),
);
store.onRead(() => {
throw Error();
});
store.write({ foo: 42 });
});

test('simple', async () => {
const result = await store.read({ foo: 1, bar: 1 });
expect(result).toEqual({ foo: 42, bar: null });
});

test('watch', async () => {
const result = store.watch({ foo: 1 });
expect((await result.next()).value).toEqual(undefined);
store.write({ foo: 44 });
expect((await result.next()).value).toEqual({ foo: 44 });
});

test('range', async () => {
const result = await store.read([{ first: 3 }, 1]);
expect(result).toEqual([42]);
});
});
35 changes: 35 additions & 0 deletions src/joinery/Node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class Leaf {
constructor(parent, key) {
this._key = key;
this._parent = parent;
this._root = parent ? parent._root : this;

if (parent) parent.addChild(this);
}
}

class Branch extends Array {
constructor(parent, key) {
super();
this._key = key;
this._parent = parent;
this._root = parent ? parent._root : this;

if (parent) parent.addChild(this);
}

addChild(child) {
this.push(child);
}
}

const root = new Branch();
console.log(Array.isArray(root));

// const foo = new Branch(root, 'foo');
// const bar = new Branch(root, 'bar');
//
// new Leaf(bar, 'baz');
//
// console.log(root);
//
62 changes: 62 additions & 0 deletions src/joinery/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import util from 'util';
import { encodeKey, keyAfter, keyBefore } from '@graffy/common';
import { isEmpty } from './common.js';

const IS_NODE = Symbol();

function copyProps(target, source) {
for (const prop in source) {
if (typeof source[prop] !== 'undefined') target[prop] = source[prop];
}
}

function makeNode(props) {
const leaf = (...children) => {
if (children.length === 1 && !children[0]?.[IS_NODE]) {
leaf.value = children[0];
return leaf;
}
const branch = [...children].sort((a, b) => (a.key < b.key ? -1 : 1));
copyProps(branch, props);
Object.defineProperty(branch, IS_NODE, { value: true });
branch[util.inspect.custom] = () =>
`${branch.key} ${util.inspect(branch.slice(0))}`;

return branch;
};
copyProps(leaf, props);
leaf.value = 1;
Object.defineProperty(leaf, IS_NODE, { value: true });
leaf[util.inspect.custom] = () => `${leaf.key} = ${leaf.value}`;

return leaf;
}

export const q = new Proxy(function () {}, {
get: (_target, key) => makeNode({ key }),
apply: (
_target,
_thisObject,
[{ first, last, after, before, since, until, ...params }],
) => {
let key = '';
let end = '\uffff';

if (typeof after !== 'undefined') key = keyAfter(encodeKey(after));
if (typeof before !== 'undefined') key = keyBefore(encodeKey(before));
if (typeof since !== 'undefined') key = encodeKey(since);
if (typeof until !== 'undefined') key = encodeKey(until);

if (last) [key, end] = [end, key];
if (!isEmpty(params)) {
const prefix = encodeKey(params) + '.';
key = prefix + key;
end = prefix + end;
}
const count = first || last;
return makeNode({ key, end, count });
},
});

export const g = q;
export const _ = q;
53 changes: 53 additions & 0 deletions src/joinery/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { _ } from './build';

export default function find(items, compare, first = 0, last = items.length) {
while (first < last) {
const ix = ((first + last) / 2) | 0;
const d = compare(items[ix]);

if (d < 0) {
first = ix + 1;
} else if (d > 0) {
last = ix;
} else {
return ix;
}
}

return first;
}

export function normalizePath(path) {
return typeof path === 'string' ? path.split('.') : path;
}

export function wrap(tree, path) {
path = normalizePath(path);
if (!path.length) return tree;

const meta = tree.meta;
delete tree.meta;

for (let i = path.length - 1; i >= 0; i--) {
tree = _[path[i]](tree);
}
tree.meta = meta;

return tree;
}

export function peel(tree, path) {
path = normalizePath(path);
let node = tree;
for (const name of path) {
const ix = find(node, ({ key }) => (key < name ? -1 : key > name ? 1 : 0));
if (!node[ix] || node[ix].key !== name) return;
node = node[ix];
}
return node;
}

export function isEmpty(object) {
for (const _ in object) return false;
return true;
}
26 changes: 26 additions & 0 deletions src/joinery/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getSelects, getUpdates, runSelects } from './sql';

import { Repeater } from '@repeaterjs/repeater';
export default (options) => (store) => {
store.on('read', read);
store.on('write', write);

const pollInterval = options.pollInterval || 1000;

function read(query) {
const plans = getSelects(query, options);
return new Repeater(async (push, stop) => {
let [result, lastUpdateTime] = await runSelects(plans);
const interval = setInterval(async () => {
push(result);
[result, lastUpdateTime] = await runSelects(plans, lastUpdateTime);
}, pollInterval);
await stop;
clearInterval(interval);
});
}

function write(change) {
const plans = getUpdates(change, options);
}
};
Loading