datafolder 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,8 @@ datafolder is a lightning fluid JSON database for high speed SSDs, featuring an
4
4
 
5
5
  ### Changelog
6
6
 
7
+ 1.0.16 Add GraphQL sample data test (test/testQyGraphQL.js) to show how to write complex queries.
8
+
7
9
  1.0.15 Use [Workers](https://nodejs.org/docs/latest/api/worker_threads.html) to implement snapshot compactor, expire service, outdated data cleaner etc.
8
10
 
9
11
  1.0.14 Expire supports specifying milliseconds.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datafolder",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "author": {
5
5
  "name": "lysh",
6
6
  "email": "datafolderdev@outlook.com"
@@ -52,7 +52,8 @@
52
52
  "scripts": {
53
53
  "ui": "start http://localhost:3628/datafolderui/index.html && node ./scripts/fastify.js",
54
54
  "airbnb": "node ./scripts/airbnb2.js && start http://localhost:3628/datafolderui/index.html && node ./scripts/fastify.js",
55
- "types:check": "tsc --noEmit"
55
+ "types:check": "tsc --noEmit",
56
+ "test": "node --test --experimental-test-coverage --test-coverage-exclude \"test/testQyAirBnB_Ubuntu.ts\" \"test/*\""
56
57
  },
57
58
  "type": "module"
58
59
  }
package/src/QyExpire.js CHANGED
@@ -65,9 +65,7 @@ class a extends e {
65
65
  onEmptyHashList(e) {
66
66
  var t, r = this.expireMap;
67
67
  for (t of e) delete r[t];
68
- i(r) && Object.assign(this, {
69
- roundedStartTime: void 0
70
- });
68
+ i(r) && (this.roundedStartTime = void 0);
71
69
  }
72
70
  onExpired(e) {
73
71
  var t, {
@@ -6596,6 +6596,7 @@
6596
6596
  this.cache.reused.set(oldTile, 2 /* Reused.DOM */);
6597
6597
  let text = new TextTile(composition.text, composition.text.nodeValue);
6598
6598
  text.flags |= 8 /* TileFlag.Composition */;
6599
+ this.pos = composition.range.toB;
6599
6600
  head.append(text);
6600
6601
  }
6601
6602
  addInlineWidget(widget, marks, openStart) {
@@ -8120,7 +8121,7 @@
8120
8121
  // (including the position after the last piece). For a text tile,
8121
8122
  // these will be character clusters, for a composite tile, these
8122
8123
  // will be child tiles.
8123
- scan(positions, getRects) {
8124
+ scan(positions, getRects, recursed = false) {
8124
8125
  let lo = 0, hi = positions.length - 1, seen = new Set();
8125
8126
  let bidi = this.bidiIn(positions[0], positions[hi]);
8126
8127
  let above, below;
@@ -8193,19 +8194,19 @@
8193
8194
  if (!closestRect) {
8194
8195
  let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
8195
8196
  this.y = (side.top + side.bottom) / 2;
8196
- return this.scan(positions, getRects);
8197
+ return this.scan(positions, getRects, true);
8197
8198
  }
8198
8199
  // Handle the case where closest matched a higher element on the
8199
8200
  // same line as an element below/above the coords
8200
- if (closestDx) {
8201
+ if (closestDx && !recursed) {
8201
8202
  let { top, bottom } = closestRect;
8202
8203
  if (above && above.bottom > (top + top + bottom) / 3) {
8203
8204
  this.y = above.bottom - 1;
8204
- return this.scan(positions, getRects);
8205
+ return this.scan(positions, getRects, true);
8205
8206
  }
8206
8207
  if (below && below.top < (top + bottom + bottom) / 3) {
8207
8208
  this.y = below.top + 1;
8208
- return this.scan(positions, getRects);
8209
+ return this.scan(positions, getRects, true);
8209
8210
  }
8210
8211
  }
8211
8212
  let ltr = (bidi ? this.dirAt(positions[closestI], 1) : this.baseDir) == Direction.LTR;
@@ -9499,7 +9500,7 @@
9499
9500
  view.inputState.compositionFirstChange = null;
9500
9501
  if (browser.chrome && browser.android) {
9501
9502
  // Delay flushing for a bit on Android because it'll often fire a
9502
- // bunch of contradictory changes in a row at end of compositon
9503
+ // bunch of contradictory changes in a row at end of composition
9503
9504
  view.observer.flushSoon();
9504
9505
  }
9505
9506
  else if (view.inputState.compositionPendingChange) {
@@ -9630,8 +9631,7 @@
9630
9631
  }
9631
9632
  refresh(whiteSpace, lineHeight, charWidth, textHeight, lineLength, knownHeights) {
9632
9633
  let lineWrapping = wrappingWhiteSpace.indexOf(whiteSpace) > -1;
9633
- let changed = Math.abs(lineHeight - this.lineHeight) > 0.3 || this.lineWrapping != lineWrapping ||
9634
- Math.abs(charWidth - this.charWidth) > 0.1;
9634
+ let changed = Math.abs(lineHeight - this.lineHeight) > 0.3 || this.lineWrapping != lineWrapping;
9635
9635
  this.lineWrapping = lineWrapping;
9636
9636
  this.lineHeight = lineHeight;
9637
9637
  this.charWidth = charWidth;
@@ -14833,11 +14833,13 @@
14833
14833
  arrow: tooltips.some(t => t.arrow),
14834
14834
  };
14835
14835
  });
14836
+ const hoverPlugin = /*@__PURE__*/Facet.define();
14836
14837
  class HoverPlugin {
14837
- constructor(view, source, field, setHover, hoverTime) {
14838
+ constructor(view, source, field, locked, setHover, hoverTime) {
14838
14839
  this.view = view;
14839
14840
  this.source = source;
14840
14841
  this.field = field;
14842
+ this.locked = locked;
14841
14843
  this.setHover = setHover;
14842
14844
  this.hoverTime = hoverTime;
14843
14845
  this.hoverTimeout = -1;
@@ -14848,7 +14850,7 @@
14848
14850
  view.dom.addEventListener("mouseleave", this.mouseleave = this.mouseleave.bind(this));
14849
14851
  view.dom.addEventListener("mousemove", this.mousemove = this.mousemove.bind(this));
14850
14852
  }
14851
- update() {
14853
+ update(update) {
14852
14854
  if (this.pending) {
14853
14855
  this.pending = null;
14854
14856
  clearTimeout(this.restartTimeout);
@@ -14892,19 +14894,29 @@
14892
14894
  let rtl = bidi && bidi.dir == Direction.RTL ? -1 : 1;
14893
14895
  side = (lastMove.x < posCoords.left ? -rtl : rtl);
14894
14896
  }
14897
+ this.activateHover(view, pos, side);
14898
+ }
14899
+ activateHover(view, pos, side, locked) {
14895
14900
  let open = this.source(view, pos, side);
14896
- if (open === null || open === void 0 ? void 0 : open.then) {
14901
+ let done = (value) => {
14902
+ if (value && !(Array.isArray(value) && !value.length)) {
14903
+ let tooltips = Array.isArray(value) ? value : [value];
14904
+ if (locked)
14905
+ this.locked.set(tooltips, locked);
14906
+ view.dispatch({ effects: this.setHover.of(tooltips) });
14907
+ }
14908
+ };
14909
+ if (open && "then" in open) {
14897
14910
  let pending = this.pending = { pos };
14898
14911
  open.then(result => {
14899
14912
  if (this.pending == pending) {
14900
14913
  this.pending = null;
14901
- if (result && !(Array.isArray(result) && !result.length))
14902
- view.dispatch({ effects: this.setHover.of(Array.isArray(result) ? result : [result]) });
14914
+ done(result);
14903
14915
  }
14904
14916
  }, e => logException(view.state, e, "hover tooltip"));
14905
14917
  }
14906
- else if (open && !(Array.isArray(open) && !open.length)) {
14907
- view.dispatch({ effects: this.setHover.of(Array.isArray(open) ? open : [open]) });
14918
+ else {
14919
+ done(open);
14908
14920
  }
14909
14921
  }
14910
14922
  get tooltip() {
@@ -14918,7 +14930,7 @@
14918
14930
  if (this.hoverTimeout < 0)
14919
14931
  this.hoverTimeout = setTimeout(this.checkHover, this.hoverTime);
14920
14932
  let { active, tooltip } = this;
14921
- if (active.length && tooltip && !isInTooltip(tooltip.dom, event) || this.pending) {
14933
+ if (active.length && !this.locked.has(active) && tooltip && !isInTooltip(tooltip.dom, event) || this.pending) {
14922
14934
  let { pos } = active[0] || this.pending, end = (_b = (_a = active[0]) === null || _a === void 0 ? void 0 : _a.end) !== null && _b !== void 0 ? _b : pos;
14923
14935
  if ((pos == end ? this.view.posAtCoords(this.lastMove) != pos
14924
14936
  : !isOverRange(this.view, pos, end, event.clientX, event.clientY))) {
@@ -14931,7 +14943,7 @@
14931
14943
  clearTimeout(this.hoverTimeout);
14932
14944
  this.hoverTimeout = -1;
14933
14945
  let { active } = this;
14934
- if (active.length) {
14946
+ if (active.length && !this.locked.has(active)) {
14935
14947
  let { tooltip } = this;
14936
14948
  let inTooltip = tooltip && tooltip.dom.contains(event.relatedTarget);
14937
14949
  if (!inTooltip)
@@ -14943,7 +14955,8 @@
14943
14955
  watchTooltipLeave(tooltip) {
14944
14956
  let watch = (event) => {
14945
14957
  tooltip.removeEventListener("mouseleave", watch);
14946
- if (this.active.length && !this.view.dom.contains(event.relatedTarget))
14958
+ let { active } = this;
14959
+ if (active.length && !this.locked.has(active) && !this.view.dom.contains(event.relatedTarget))
14947
14960
  this.view.dispatch({ effects: this.setHover.of([]) });
14948
14961
  };
14949
14962
  tooltip.addEventListener("mouseleave", watch);
@@ -14994,49 +15007,85 @@
14994
15007
  */
14995
15008
  function hoverTooltip(source, options = {}) {
14996
15009
  let setHover = StateEffect.define();
15010
+ // This would be better stored in the state field, but we've set
15011
+ // down the type of the field in our interface, so it's indirectly
15012
+ // stored by array identity.
15013
+ let locked = new WeakMap();
14997
15014
  let hoverState = StateField.define({
14998
15015
  create() { return []; },
14999
15016
  update(value, tr) {
15017
+ let lock = locked.get(value);
15000
15018
  if (value.length) {
15001
15019
  if (options.hideOnChange && (tr.docChanged || tr.selection))
15002
15020
  value = [];
15021
+ else if (lock && lock(tr))
15022
+ value = [];
15003
15023
  else if (options.hideOn)
15004
15024
  value = value.filter(v => !options.hideOn(tr, v));
15005
- if (tr.docChanged) {
15006
- let mapped = [];
15007
- for (let tooltip of value) {
15008
- let newPos = tr.changes.mapPos(tooltip.pos, -1, MapMode.TrackDel);
15009
- if (newPos != null) {
15010
- let copy = Object.assign(Object.create(null), tooltip);
15011
- copy.pos = newPos;
15012
- if (copy.end != null)
15013
- copy.end = tr.changes.mapPos(copy.end);
15014
- mapped.push(copy);
15015
- }
15025
+ }
15026
+ if (tr.docChanged && value.length) {
15027
+ let mapped = [];
15028
+ for (let tooltip of value) {
15029
+ let newPos = tr.changes.mapPos(tooltip.pos, -1, MapMode.TrackDel);
15030
+ if (newPos != null) {
15031
+ let copy = Object.assign(Object.create(null), tooltip);
15032
+ copy.pos = newPos;
15033
+ if (copy.end != null)
15034
+ copy.end = tr.changes.mapPos(copy.end);
15035
+ mapped.push(copy);
15016
15036
  }
15017
- value = mapped;
15018
15037
  }
15038
+ value = mapped;
15019
15039
  }
15020
15040
  for (let effect of tr.effects) {
15021
- if (effect.is(setHover))
15041
+ if (effect.is(setHover)) {
15022
15042
  value = effect.value;
15023
- if (effect.is(closeHoverTooltipEffect))
15043
+ lock = undefined;
15044
+ }
15045
+ if (effect.is(closeHoverTooltipEffect) && !effect.value || effect.value == hoverState)
15024
15046
  value = [];
15025
15047
  }
15048
+ if (value.length && lock)
15049
+ locked.set(value, lock);
15026
15050
  return value;
15027
15051
  },
15028
15052
  provide: f => showHoverTooltip.from(f)
15029
15053
  });
15054
+ const plugin = ViewPlugin.define(view => new HoverPlugin(view, source, hoverState, locked, setHover, options.hoverTime || 300 /* Hover.Time */));
15030
15055
  return {
15031
15056
  active: hoverState,
15032
15057
  extension: [
15033
15058
  hoverState,
15034
- ViewPlugin.define(view => new HoverPlugin(view, source, hoverState, setHover, options.hoverTime || 300 /* Hover.Time */)),
15059
+ plugin,
15060
+ hoverPlugin.of(plugin),
15035
15061
  showHoverTooltipHost
15036
15062
  ]
15037
15063
  };
15038
15064
  }
15039
15065
  /**
15066
+ Activate hover tooltips for the given position and side. If you
15067
+ provide a specific hover tooltip (the value returned from
15068
+ [`hoverTooltip`](https://codemirror.net/6/docs/ref/#view.hoverTooltip)), only that one will be
15069
+ activated. If not given, all hover tooltips at the given position
15070
+ are triggered.
15071
+
15072
+ Note that tooltips opened this way don't close automatically, and
15073
+ you'll want to pass an `until` callback or use
15074
+ [`closeHoverTooltip`](https://codemirror.net/6/docs/ref/#view.closeHoverTooltip)/[`closeHoverTooltips`](https://codemirror.net/6/docs/ref/#view.closeHoverTooltips)
15075
+ to deactivate them.
15076
+ */
15077
+ function activateHover(view, pos, side, options = {}) {
15078
+ var _a;
15079
+ let plugins = view.state.facet(hoverPlugin).map(p => view.plugin(p)).filter((p) => !!p);
15080
+ if (options.tooltip && options.tooltip.active) {
15081
+ let found = plugins.find(p => p.field == options.tooltip.active);
15082
+ if (found)
15083
+ plugins = [found];
15084
+ }
15085
+ for (let plugin of plugins)
15086
+ plugin.activateHover(view, pos, side, (_a = options.until) !== null && _a !== void 0 ? _a : (() => false));
15087
+ }
15088
+ /**
15040
15089
  Get the active tooltip view for a given tooltip, if available.
15041
15090
  */
15042
15091
  function getTooltip(view, tooltip) {
@@ -23521,8 +23570,8 @@
23521
23570
  let off = Math.floor(selected / max);
23522
23571
  return { from: off * max, to: (off + 1) * max };
23523
23572
  }
23524
- let off = Math.floor((total - selected) / max);
23525
- return { from: total - (off + 1) * max, to: total - off * max };
23573
+ let off = Math.ceil((total - selected) / max);
23574
+ return { from: total - off * max, to: total - (off - 1) * max };
23526
23575
  }
23527
23576
  class CompletionTooltip {
23528
23577
  constructor(view, stateField, applyCompletion) {
@@ -24381,7 +24430,8 @@
24381
24430
  content: '"···"',
24382
24431
  opacity: 0.5,
24383
24432
  display: "block",
24384
- textAlign: "center"
24433
+ textAlign: "center",
24434
+ cursor: "pointer",
24385
24435
  },
24386
24436
  ".cm-tooltip.cm-completionInfo": {
24387
24437
  position: "absolute",
@@ -25229,6 +25279,10 @@
25229
25279
  return false;
25230
25280
  }
25231
25281
  view.dispatch({ selection: { anchor: next.from, head: next.to }, scrollIntoView: true });
25282
+ activateHover(view, next.from, 1, {
25283
+ tooltip: lintHover,
25284
+ until: tr => tr.docChanged || tr.newSelection.main.head < next.from || tr.newSelection.main.head > next.to
25285
+ });
25232
25286
  return true;
25233
25287
  };
25234
25288
  /**
@@ -25538,7 +25592,7 @@
25538
25592
  backgroundRepeat: "repeat-x",
25539
25593
  paddingBottom: "0.7px",
25540
25594
  },
25541
- ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#d11") },
25595
+ ".cm-lintRange-error": { backgroundImage: /*@__PURE__*/underline("#f11") },
25542
25596
  ".cm-lintRange-warning": { backgroundImage: /*@__PURE__*/underline("orange") },
25543
25597
  ".cm-lintRange-info": { backgroundImage: /*@__PURE__*/underline("#999") },
25544
25598
  ".cm-lintRange-hint": { backgroundImage: /*@__PURE__*/underline("#66d") },
@@ -25619,6 +25673,7 @@
25619
25673
  }
25620
25674
  return sev;
25621
25675
  }
25676
+ const lintHover = /*@__PURE__*/hoverTooltip(lintTooltip, { hideOn: hideTooltip });
25622
25677
  const lintExtensions = [
25623
25678
  lintState,
25624
25679
  /*@__PURE__*/EditorView.decorations.compute([lintState], state => {
@@ -25627,7 +25682,7 @@
25627
25682
  activeMark.range(selected.from, selected.to)
25628
25683
  ]);
25629
25684
  }),
25630
- /*@__PURE__*/hoverTooltip(lintTooltip, { hideOn: hideTooltip }),
25685
+ lintHover,
25631
25686
  baseTheme
25632
25687
  ];
25633
25688
 
@@ -0,0 +1,198 @@
1
+ import test from "node:test";
2
+ import { deepEqual } from "node:assert/strict";
3
+ import { QyDB } from "../src/QyDB.js";
4
+ import { logger } from "../src/QyLogger.js";
5
+ logger.level = "warn";
6
+ const graphQLData = {
7
+ Characters: {
8
+ luke: {
9
+ type: "Human",
10
+ id: "1000",
11
+ name: "Luke Skywalker",
12
+ friends: ["1002", "1003", "2000", "2001"],
13
+ appearsIn: [4, 5, 6],
14
+ homePlanet: "Tatooine",
15
+ },
16
+ vader: {
17
+ type: "Human",
18
+ id: "1001",
19
+ name: "Darth Vader",
20
+ friends: ["1004"],
21
+ appearsIn: [4, 5, 6],
22
+ homePlanet: "Tatooine",
23
+ },
24
+ han: {
25
+ type: "Human",
26
+ id: "1002",
27
+ name: "Han Solo",
28
+ friends: ["1000", "1003", "2001"],
29
+ appearsIn: [4, 5, 6],
30
+ },
31
+ leia: {
32
+ type: "Human",
33
+ id: "1003",
34
+ name: "Leia Organa",
35
+ friends: ["1000", "1002", "2000", "2001"],
36
+ appearsIn: [4, 5, 6],
37
+ homePlanet: "Alderaan",
38
+ },
39
+ tarkin: {
40
+ type: "Human",
41
+ id: "1004",
42
+ name: "Wilhuff Tarkin",
43
+ friends: ["1001"],
44
+ appearsIn: [4],
45
+ },
46
+ threepio: {
47
+ type: "Droid",
48
+ id: "2000",
49
+ name: "C-3PO",
50
+ friends: ["1000", "1002", "1003", "2001"],
51
+ appearsIn: [4, 5, 6],
52
+ primaryFunction: "Protocol",
53
+ },
54
+ artoo: {
55
+ type: "Droid",
56
+ id: "2001",
57
+ name: "R2-D2",
58
+ friends: ["1000", "1002", "1003"],
59
+ appearsIn: [4, 5],
60
+ primaryFunction: "Astromech",
61
+ },
62
+ },
63
+ };
64
+ function insertData(qyDB) {
65
+ const { batch } = qyDB;
66
+ const { Characters } = graphQLData;
67
+ for (const key in Characters) {
68
+ const character = Characters[key];
69
+ const { id } = character;
70
+ batch.put(["Characters", id], character);
71
+ }
72
+ batch.run();
73
+ }
74
+ function query1(qyDB) {
75
+ deepEqual(qyDB.queryFiles("Characters", { friends: { $or: ["1000", "1004"] } }).map((x) => x.view(["id", "name"])), [
76
+ { id: "1002", name: "Han Solo" },
77
+ { id: "1003", name: "Leia Organa" },
78
+ { id: "2000", name: "C-3PO" },
79
+ { id: "2001", name: "R2-D2" },
80
+ { id: "1001", name: "Darth Vader" },
81
+ ]);
82
+ deepEqual(qyDB.queryFiles("Characters", { friends: { $and: ["1002", "2001"] } }).map((x) => x.view(["id", "name"])), [
83
+ { id: "1000", name: "Luke Skywalker" },
84
+ { id: "1003", name: "Leia Organa" },
85
+ { id: "2000", name: "C-3PO" },
86
+ ]);
87
+ deepEqual(qyDB.queryFiles("Characters", { friends: ["1002", "2001"] }).map((x) => x.view(["id", "name"])), [
88
+ { id: "1000", name: "Luke Skywalker" },
89
+ { id: "1003", name: "Leia Organa" },
90
+ { id: "2000", name: "C-3PO" },
91
+ ]);
92
+ }
93
+ function query2(qyDB) {
94
+ deepEqual(qyDB.queryFiles("Characters", { friends: { $or: ["1000"] } }).map((x) => x.view(["id", "name"])), [
95
+ { id: "1002", name: "Han Solo" },
96
+ { id: "1003", name: "Leia Organa" },
97
+ { id: "2000", name: "C-3PO" },
98
+ { id: "2001", name: "R2-D2" },
99
+ ]);
100
+ deepEqual(qyDB.queryFiles("Characters", { friends: { $or: ["1002", "1003"] } }).map((x) => x.view(["id", "name"])), [
101
+ { id: "1000", name: "Luke Skywalker" },
102
+ { id: "1003", name: "Leia Organa" },
103
+ { id: "2000", name: "C-3PO" },
104
+ { id: "2001", name: "R2-D2" },
105
+ { id: "1002", name: "Han Solo" },
106
+ ]);
107
+ deepEqual(qyDB
108
+ .queryFiles("Characters", {
109
+ friends: { $link: "Characters", $spec: { friends: { $link: "Characters", $spec: { friends: "1003" } } } },
110
+ })
111
+ .map((x) => x.view(["id", "name"])), [
112
+ { id: "1002", name: "Han Solo" },
113
+ { id: "1003", name: "Leia Organa" },
114
+ { id: "2000", name: "C-3PO" },
115
+ { id: "2001", name: "R2-D2" },
116
+ { id: "1000", name: "Luke Skywalker" },
117
+ ]);
118
+ deepEqual(qyDB
119
+ .queryFiles("Characters", {
120
+ friends: { $link: "Characters", $spec: { friends: [{ $link: "Characters", $spec: { friends: "1003" } }] } },
121
+ })
122
+ .map((x) => x.view(["id", "name"])), [
123
+ { id: "1002", name: "Han Solo" },
124
+ { id: "1003", name: "Leia Organa" },
125
+ { id: "2000", name: "C-3PO" },
126
+ { id: "2001", name: "R2-D2" },
127
+ { id: "1000", name: "Luke Skywalker" },
128
+ ]);
129
+ }
130
+ function query3(qyDB) {
131
+ deepEqual(qyDB.queryFiles("Characters", { friends: { $or: ["1000"] } }).map((x) => x.view(["id", "name"])), [
132
+ { id: "1002", name: "Han Solo" },
133
+ { id: "1003", name: "Leia Organa" },
134
+ { id: "2000", name: "C-3PO" },
135
+ { id: "2001", name: "R2-D2" },
136
+ ]);
137
+ deepEqual(qyDB
138
+ .queryFiles("Characters", { friends: (x) => x.includes("1002") || x.includes("1003") })
139
+ .map((x) => x.view(["id", "name"])), [
140
+ { id: "1000", name: "Luke Skywalker" },
141
+ { id: "1003", name: "Leia Organa" },
142
+ { id: "2000", name: "C-3PO" },
143
+ { id: "2001", name: "R2-D2" },
144
+ { id: "1002", name: "Han Solo" },
145
+ ]);
146
+ deepEqual(qyDB
147
+ .queryFiles("Characters", {
148
+ appearsIn: 6,
149
+ friends: {
150
+ $link: "Characters",
151
+ $spec: {
152
+ friends: {
153
+ $link: "Characters",
154
+ $spec: { friends: (x) => x.includes("1003"), appearsIn: (x) => x.includes(6) },
155
+ },
156
+ },
157
+ },
158
+ })
159
+ .map((x) => x.view(["id", "name"])), [
160
+ { id: "1000", name: "Luke Skywalker" },
161
+ { id: "1002", name: "Han Solo" },
162
+ { id: "1003", name: "Leia Organa" },
163
+ { id: "2000", name: "C-3PO" },
164
+ ]);
165
+ deepEqual(qyDB
166
+ .queryFiles("Characters", {
167
+ appearsIn: 6,
168
+ type: "Droid",
169
+ friends: {
170
+ $link: "Characters",
171
+ $spec: {
172
+ type: "Droid",
173
+ friends: {
174
+ $link: "Characters",
175
+ $spec: { friends: (x) => x.includes("1003"), appearsIn: (x) => x.includes(6) },
176
+ },
177
+ },
178
+ },
179
+ })
180
+ .map((x) => x.view(["id", "name"])), [{ id: "2000", name: "C-3PO" }]);
181
+ }
182
+ test("test QyGraphQL", { skip: false }, async (context) => {
183
+ context.after(async (t) => {
184
+ await qyDB.stop();
185
+ });
186
+ const dataFolder = "./tmp/QyGraphQL/1";
187
+ const qyDB = new QyDB(dataFolder);
188
+ await qyDB.start();
189
+ insertData(qyDB);
190
+ query1(qyDB);
191
+ query2(qyDB);
192
+ query3(qyDB);
193
+ await qyDB.stop();
194
+ await qyDB.start();
195
+ query1(qyDB);
196
+ query2(qyDB);
197
+ query3(qyDB);
198
+ });