@adobe/data 0.9.62 → 0.9.63
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/dist/ecs/database/create-plugin.d.ts +1 -1
- package/dist/ecs/database/database.index.performance.test.js +5 -5
- package/dist/ecs/database/database.index.performance.test.js.map +1 -1
- package/dist/ecs/database/database.index.test.js +193 -68
- package/dist/ecs/database/database.index.test.js.map +1 -1
- package/dist/ecs/database/database.index.type-test.js +34 -10
- package/dist/ecs/database/database.index.type-test.js.map +1 -1
- package/dist/ecs/database/index-registry/create-index-registry.d.ts +38 -9
- package/dist/ecs/database/index-registry/create-index-registry.js +67 -20
- package/dist/ecs/database/index-registry/create-index-registry.js.map +1 -1
- package/dist/ecs/database/index-registry/create-index.d.ts +10 -2
- package/dist/ecs/database/index-registry/create-index.js +44 -47
- package/dist/ecs/database/index-registry/create-index.js.map +1 -1
- package/dist/ecs/database/public/create-database.js +5 -7
- package/dist/ecs/database/public/create-database.js.map +1 -1
- package/dist/ecs/store/core/select-entities.js +6 -3
- package/dist/ecs/store/core/select-entities.js.map +1 -1
- package/dist/ecs/store/index-types.d.ts +43 -17
- package/dist/ecs/store/public/create-store.js +15 -5
- package/dist/ecs/store/public/create-store.js.map +1 -1
- package/dist/ecs/store/store.d.ts +2 -2
- package/dist/functions/compare.d.ts +13 -0
- package/dist/functions/compare.js +15 -0
- package/dist/functions/compare.js.map +1 -0
- package/dist/functions/compare.test.d.ts +1 -0
- package/dist/functions/compare.test.js +24 -0
- package/dist/functions/compare.test.js.map +1 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/references/data-lit/package.json +1 -1
- package/references/data-lit-tictactoe/package.json +1 -1
- package/references/data-react/package.json +1 -1
- package/references/data-react-hello/package.json +1 -1
- package/references/data-react-pixie/package.json +1 -1
|
@@ -21,17 +21,17 @@ describe("Pattern 1 — single-column unique lookup (byEmail)", () => {
|
|
|
21
21
|
it("get returns the entity for a known scalar key", () => {
|
|
22
22
|
const db = Database.create(plugin());
|
|
23
23
|
const e = db.transactions.add("alice@a.com");
|
|
24
|
-
expect(db.indexes.byEmail.get("alice@a.com")).toBe(e);
|
|
24
|
+
expect(db.indexes.byEmail.get({ email: "alice@a.com" })).toBe(e);
|
|
25
25
|
});
|
|
26
26
|
it("get returns null for an unknown key (known absent)", () => {
|
|
27
27
|
const db = Database.create(plugin());
|
|
28
|
-
expect(db.indexes.byEmail.get("missing@x.com")).toBeNull();
|
|
28
|
+
expect(db.indexes.byEmail.get({ email: "missing@x.com" })).toBeNull();
|
|
29
29
|
});
|
|
30
30
|
it("duplicate insert throws atomically — original row stays intact", () => {
|
|
31
31
|
const db = Database.create(plugin());
|
|
32
32
|
const first = db.transactions.add("shared@x.com");
|
|
33
33
|
expect(() => db.transactions.add("shared@x.com")).toThrow(/Unique index conflict/);
|
|
34
|
-
expect(db.indexes.byEmail.get("shared@x.com")).toBe(first);
|
|
34
|
+
expect(db.indexes.byEmail.get({ email: "shared@x.com" })).toBe(first);
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
describe("Pattern 2 — multi-column compound unique (playerSlot)", () => {
|
|
@@ -84,21 +84,21 @@ describe("Pattern 3 — non-unique by single column (childrenOf)", () => {
|
|
|
84
84
|
const a = db.transactions.add({ parent: 7, label: "a" });
|
|
85
85
|
const b = db.transactions.add({ parent: 7, label: "b" });
|
|
86
86
|
db.transactions.add({ parent: 9, label: "c" });
|
|
87
|
-
expect([...db.indexes.childrenOf.find(7)].sort()).toEqual([a, b].sort());
|
|
87
|
+
expect([...db.indexes.childrenOf.find({ parent: 7 })].sort()).toEqual([a, b].sort());
|
|
88
88
|
});
|
|
89
89
|
it("delete removes the entity from the bucket", () => {
|
|
90
90
|
const db = Database.create(plugin());
|
|
91
91
|
const e = db.transactions.add({ parent: 7, label: "a" });
|
|
92
|
-
expect(db.indexes.childrenOf.find(7)).toEqual([e]);
|
|
92
|
+
expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([e]);
|
|
93
93
|
db.transactions.delete(e);
|
|
94
|
-
expect(db.indexes.childrenOf.find(7)).toEqual([]);
|
|
94
|
+
expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([]);
|
|
95
95
|
});
|
|
96
96
|
it("update moves the entity between buckets", () => {
|
|
97
97
|
const db = Database.create(plugin());
|
|
98
98
|
const e = db.transactions.add({ parent: 7, label: "a" });
|
|
99
99
|
db.transactions.move({ entity: e, newParent: 9 });
|
|
100
|
-
expect(db.indexes.childrenOf.find(7)).toEqual([]);
|
|
101
|
-
expect(db.indexes.childrenOf.find(9)).toEqual([e]);
|
|
100
|
+
expect(db.indexes.childrenOf.find({ parent: 7 })).toEqual([]);
|
|
101
|
+
expect(db.indexes.childrenOf.find({ parent: 9 })).toEqual([e]);
|
|
102
102
|
});
|
|
103
103
|
it("`get` is absent on non-unique handles at runtime and types", () => {
|
|
104
104
|
const db = Database.create(plugin());
|
|
@@ -129,7 +129,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
|
|
|
129
129
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
130
130
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
131
131
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
132
|
-
expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, b, c]);
|
|
132
|
+
expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, b, c]);
|
|
133
133
|
});
|
|
134
134
|
it("update on the sort key repositions the entity in place", () => {
|
|
135
135
|
const db = Database.create(plugin());
|
|
@@ -137,9 +137,9 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
|
|
|
137
137
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
138
138
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
139
139
|
db.transactions.move({ entity: b, fractIndex: "z" });
|
|
140
|
-
expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c, b]);
|
|
140
|
+
expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, c, b]);
|
|
141
141
|
db.transactions.move({ entity: a, fractIndex: "d" });
|
|
142
|
-
expect(db.indexes.orderedChildrenOf.find(7)).toEqual([c, a, b]);
|
|
142
|
+
expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([c, a, b]);
|
|
143
143
|
});
|
|
144
144
|
it("delete preserves sort order of the remaining entities", () => {
|
|
145
145
|
const db = Database.create(plugin());
|
|
@@ -147,7 +147,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
|
|
|
147
147
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
148
148
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
149
149
|
db.transactions.delete(b);
|
|
150
|
-
expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c]);
|
|
150
|
+
expect(db.indexes.orderedChildrenOf.find({ parent: 7 })).toEqual([a, c]);
|
|
151
151
|
});
|
|
152
152
|
it("populates and sorts when the index is registered after data exists", () => {
|
|
153
153
|
const base = Database.Plugin.create({
|
|
@@ -171,7 +171,7 @@ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
|
|
|
171
171
|
},
|
|
172
172
|
});
|
|
173
173
|
const ext = db.extend(indexed);
|
|
174
|
-
expect(ext.indexes.orderedChildrenOf.find(5)).toEqual([a, b, c]);
|
|
174
|
+
expect(ext.indexes.orderedChildrenOf.find({ parent: 5 })).toEqual([a, b, c]);
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
177
|
describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
@@ -200,7 +200,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
200
200
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
201
201
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
202
202
|
const emissions = [];
|
|
203
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
203
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
204
204
|
expect(emissions).toEqual([[a, b, c]]);
|
|
205
205
|
unsub();
|
|
206
206
|
});
|
|
@@ -209,7 +209,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
209
209
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
210
210
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
211
211
|
const emissions = [];
|
|
212
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
212
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
213
213
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
214
214
|
await Promise.resolve();
|
|
215
215
|
expect(emissions[0]).toEqual([a, c]);
|
|
@@ -222,7 +222,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
222
222
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
223
223
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
224
224
|
const emissions = [];
|
|
225
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
225
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
226
226
|
expect(emissions[0]).toEqual([a, b, c]);
|
|
227
227
|
// Reorder-only transaction: parent (the bucket key) is untouched, so a
|
|
228
228
|
// `where`-only observe.select would never see this. The index does.
|
|
@@ -236,7 +236,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
236
236
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
237
237
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
238
238
|
const emissions = [];
|
|
239
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
239
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
240
240
|
db.transactions.delete(b);
|
|
241
241
|
await Promise.resolve();
|
|
242
242
|
expect(emissions[emissions.length - 1]).toEqual([a]);
|
|
@@ -247,7 +247,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
247
247
|
db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
248
248
|
const other = db.transactions.add({ parent: 9, fractIndex: "a" });
|
|
249
249
|
const emissions = [];
|
|
250
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
250
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
251
251
|
expect(emissions).toHaveLength(1);
|
|
252
252
|
// A child of a *different* parent moves. Same component (`fractIndex`)
|
|
253
253
|
// changes, so the observer is woken and recomputes — but its own
|
|
@@ -263,8 +263,8 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
263
263
|
const m = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
264
264
|
const fromSeven = [];
|
|
265
265
|
const toNine = [];
|
|
266
|
-
const unsub7 = db.indexes.orderedChildrenOf.observe(7)((e) => fromSeven.push([...e]));
|
|
267
|
-
const unsub9 = db.indexes.orderedChildrenOf.observe(9)((e) => toNine.push([...e]));
|
|
266
|
+
const unsub7 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => fromSeven.push([...e]));
|
|
267
|
+
const unsub9 = db.indexes.orderedChildrenOf.observe({ parent: 9 })((e) => toNine.push([...e]));
|
|
268
268
|
db.transactions.reparent({ entity: m, parent: 9 });
|
|
269
269
|
await Promise.resolve();
|
|
270
270
|
expect(fromSeven[fromSeven.length - 1]).toEqual([a]);
|
|
@@ -276,7 +276,7 @@ describe("Pattern 4 — observe(arg): reactive sorted bucket view", () => {
|
|
|
276
276
|
const db = Database.create(plugin());
|
|
277
277
|
db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
278
278
|
const emissions = [];
|
|
279
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((entities) => { emissions.push([...entities]); });
|
|
279
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((entities) => { emissions.push([...entities]); });
|
|
280
280
|
expect(emissions).toHaveLength(1);
|
|
281
281
|
unsub();
|
|
282
282
|
db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
@@ -304,7 +304,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
304
304
|
it("emits [] for an initially-empty bucket, then the entity once populated", async () => {
|
|
305
305
|
const db = Database.create(sortedPlugin());
|
|
306
306
|
const emissions = [];
|
|
307
|
-
const unsub = db.indexes.orderedChildrenOf.observe(42)((e) => emissions.push([...e]));
|
|
307
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 42 })((e) => emissions.push([...e]));
|
|
308
308
|
expect(emissions).toEqual([[]]);
|
|
309
309
|
const e = db.transactions.add({ parent: 42, fractIndex: "a" });
|
|
310
310
|
await Promise.resolve();
|
|
@@ -315,7 +315,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
315
315
|
const db = Database.create(sortedPlugin());
|
|
316
316
|
const only = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
317
317
|
const emissions = [];
|
|
318
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
|
|
318
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
|
|
319
319
|
db.transactions.delete(only);
|
|
320
320
|
await Promise.resolve();
|
|
321
321
|
expect(emissions[emissions.length - 1]).toEqual([]);
|
|
@@ -326,8 +326,8 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
326
326
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
327
327
|
const first = [];
|
|
328
328
|
const second = [];
|
|
329
|
-
const unsub1 = db.indexes.orderedChildrenOf.observe(7)((e) => first.push([...e]));
|
|
330
|
-
const unsub2 = db.indexes.orderedChildrenOf.observe(7)((e) => second.push([...e]));
|
|
329
|
+
const unsub1 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => first.push([...e]));
|
|
330
|
+
const unsub2 = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => second.push([...e]));
|
|
331
331
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
332
332
|
await Promise.resolve();
|
|
333
333
|
expect(first[first.length - 1]).toEqual([a, b]);
|
|
@@ -339,7 +339,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
339
339
|
const db = Database.create(sortedPlugin());
|
|
340
340
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
341
341
|
const emissions = [];
|
|
342
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
|
|
342
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
|
|
343
343
|
// Three synchronous transactions, no awaits in between.
|
|
344
344
|
const c = db.transactions.add({ parent: 7, fractIndex: "c" });
|
|
345
345
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
@@ -354,7 +354,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
354
354
|
const db = Database.create(sortedPlugin());
|
|
355
355
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
356
356
|
const emissions = [];
|
|
357
|
-
const unsub = db.indexes.orderedChildrenOf.observe(7)((e) => emissions.push([...e]));
|
|
357
|
+
const unsub = db.indexes.orderedChildrenOf.observe({ parent: 7 })((e) => emissions.push([...e]));
|
|
358
358
|
// Move the only child to a new sort key and back within the same
|
|
359
359
|
// microtask window: the bucket is woken but its final sequence is
|
|
360
360
|
// identical, so no second emission.
|
|
@@ -368,7 +368,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
368
368
|
const db = Database.create(sortedPlugin());
|
|
369
369
|
const a = db.transactions.add({ parent: 7, fractIndex: "a" });
|
|
370
370
|
const emissions = [];
|
|
371
|
-
const unsub = db.indexes.childrenOf.observe(7)((e) => emissions.push([...e].sort()));
|
|
371
|
+
const unsub = db.indexes.childrenOf.observe({ parent: 7 })((e) => emissions.push([...e].sort()));
|
|
372
372
|
expect(emissions[0]).toEqual([a]);
|
|
373
373
|
const b = db.transactions.add({ parent: 7, fractIndex: "b" });
|
|
374
374
|
await Promise.resolve();
|
|
@@ -402,7 +402,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
402
402
|
const low = db.transactions.add({ owner: 1, priority: 1, due: 100 });
|
|
403
403
|
const high = db.transactions.add({ owner: 1, priority: 3, due: 50 });
|
|
404
404
|
const emissions = [];
|
|
405
|
-
const unsub = db.indexes.tasksByPriority.observe(1)((e) => emissions.push([...e]));
|
|
405
|
+
const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
|
|
406
406
|
// priority desc → [high, low].
|
|
407
407
|
expect(emissions[0]).toEqual([high, low]);
|
|
408
408
|
db.transactions.setPriority({ entity: low, priority: 5 });
|
|
@@ -416,7 +416,7 @@ describe("observe(arg) — additional edge cases", () => {
|
|
|
416
416
|
const early = db.transactions.add({ owner: 1, priority: 2, due: 10 });
|
|
417
417
|
const late = db.transactions.add({ owner: 1, priority: 2, due: 20 });
|
|
418
418
|
const emissions = [];
|
|
419
|
-
const unsub = db.indexes.tasksByPriority.observe(1)((e) => emissions.push([...e]));
|
|
419
|
+
const unsub = db.indexes.tasksByPriority.observe({ owner: 1 })((e) => emissions.push([...e]));
|
|
420
420
|
expect(emissions[0]).toEqual([early, late]);
|
|
421
421
|
// Push `early`'s due past `late`'s — flips the tie-break order.
|
|
422
422
|
db.transactions.setDue({ entity: early, due: 30 });
|
|
@@ -444,36 +444,36 @@ describe("Pattern 5 — multi-value (array column → fan-out): tasksByAssignee"
|
|
|
444
444
|
it("each array element becomes its own bucket entry", () => {
|
|
445
445
|
const db = Database.create(plugin());
|
|
446
446
|
const e = db.transactions.add({ title: "ship", assigned: ["joe", "bob"] });
|
|
447
|
-
expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
|
|
448
|
-
expect(db.indexes.tasksByAssignee.find("bob")).toEqual([e]);
|
|
449
|
-
expect(db.indexes.tasksByAssignee.find("carol")).toEqual([]);
|
|
447
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([e]);
|
|
448
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([e]);
|
|
449
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "carol" })).toEqual([]);
|
|
450
450
|
});
|
|
451
451
|
it("multiple entities sharing an element are both returned", () => {
|
|
452
452
|
const db = Database.create(plugin());
|
|
453
453
|
const a = db.transactions.add({ title: "A", assigned: ["joe", "bob"] });
|
|
454
454
|
const b = db.transactions.add({ title: "B", assigned: ["joe", "carol"] });
|
|
455
455
|
db.transactions.add({ title: "C", assigned: ["diane"] });
|
|
456
|
-
expect([...db.indexes.tasksByAssignee.find("joe")].sort()).toEqual([a, b].sort());
|
|
456
|
+
expect([...db.indexes.tasksByAssignee.find({ assigned: "joe" })].sort()).toEqual([a, b].sort());
|
|
457
457
|
});
|
|
458
458
|
it("empty array contributes no bucket entries", () => {
|
|
459
459
|
const db = Database.create(plugin());
|
|
460
460
|
db.transactions.add({ title: "Unassigned", assigned: [] });
|
|
461
|
-
expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
|
|
461
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([]);
|
|
462
462
|
});
|
|
463
463
|
it("update set-diffs the bucket membership", () => {
|
|
464
464
|
const db = Database.create(plugin());
|
|
465
465
|
const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
|
|
466
466
|
db.transactions.reassign({ entity: e, assigned: ["joe", "carol"] });
|
|
467
|
-
expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
|
|
468
|
-
expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
|
|
469
|
-
expect(db.indexes.tasksByAssignee.find("carol")).toEqual([e]);
|
|
467
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([e]);
|
|
468
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([]);
|
|
469
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "carol" })).toEqual([e]);
|
|
470
470
|
});
|
|
471
471
|
it("delete drops the entity from every bucket it was in", () => {
|
|
472
472
|
const db = Database.create(plugin());
|
|
473
473
|
const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
|
|
474
474
|
db.transactions.delete(e);
|
|
475
|
-
expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
|
|
476
|
-
expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
|
|
475
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "joe" })).toEqual([]);
|
|
476
|
+
expect(db.indexes.tasksByAssignee.find({ assigned: "bob" })).toEqual([]);
|
|
477
477
|
});
|
|
478
478
|
});
|
|
479
479
|
describe("Pattern 6 — computed scalar (byEmailCi)", () => {
|
|
@@ -481,7 +481,7 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
|
|
|
481
481
|
components: { email: { type: "string" } },
|
|
482
482
|
archetypes: { User: ["email"] },
|
|
483
483
|
indexes: {
|
|
484
|
-
byEmailCi: { key: (
|
|
484
|
+
byEmailCi: { key: { email: (c) => c.email.toLowerCase() }, components: ["email"] },
|
|
485
485
|
},
|
|
486
486
|
transactions: {
|
|
487
487
|
add: (t, email) => t.archetypes.User.insert({ email }),
|
|
@@ -492,23 +492,23 @@ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
|
|
|
492
492
|
it("looks up by the computed (lowercased) key", () => {
|
|
493
493
|
const db = Database.create(plugin());
|
|
494
494
|
const e = db.transactions.add("Alice@Example.com");
|
|
495
|
-
expect(db.indexes.byEmailCi.find("alice@example.com")).toEqual([e]);
|
|
495
|
+
expect(db.indexes.byEmailCi.find({ email: "alice@example.com" })).toEqual([e]);
|
|
496
496
|
// The original-case input is not a key.
|
|
497
|
-
expect(db.indexes.byEmailCi.find("Alice@Example.com")).toEqual([]);
|
|
497
|
+
expect(db.indexes.byEmailCi.find({ email: "Alice@Example.com" })).toEqual([]);
|
|
498
498
|
});
|
|
499
499
|
it("update re-derives the key", () => {
|
|
500
500
|
const db = Database.create(plugin());
|
|
501
501
|
const e = db.transactions.add("alice@a.com");
|
|
502
|
-
expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([e]);
|
|
502
|
+
expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([e]);
|
|
503
503
|
db.transactions.rename({ entity: e, email: "alice@b.com" });
|
|
504
|
-
expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
|
|
505
|
-
expect(db.indexes.byEmailCi.find("alice@b.com")).toEqual([e]);
|
|
504
|
+
expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([]);
|
|
505
|
+
expect(db.indexes.byEmailCi.find({ email: "alice@b.com" })).toEqual([e]);
|
|
506
506
|
});
|
|
507
507
|
it("delete removes the computed bucket entry", () => {
|
|
508
508
|
const db = Database.create(plugin());
|
|
509
509
|
const e = db.transactions.add("alice@a.com");
|
|
510
510
|
db.transactions.delete(e);
|
|
511
|
-
expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
|
|
511
|
+
expect(db.indexes.byEmailCi.find({ email: "alice@a.com" })).toEqual([]);
|
|
512
512
|
});
|
|
513
513
|
});
|
|
514
514
|
describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
|
|
@@ -517,7 +517,9 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
|
|
|
517
517
|
archetypes: { Doc: ["body"] },
|
|
518
518
|
indexes: {
|
|
519
519
|
docsByKeyword: {
|
|
520
|
-
key:
|
|
520
|
+
key: {
|
|
521
|
+
keyword: (c) => c.body.toLowerCase().split(/\s+/).filter((s) => s.length > 0),
|
|
522
|
+
},
|
|
521
523
|
components: ["body"],
|
|
522
524
|
},
|
|
523
525
|
},
|
|
@@ -528,14 +530,14 @@ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
|
|
|
528
530
|
it("fans out each computed array element into its own bucket", () => {
|
|
529
531
|
const db = Database.create(plugin());
|
|
530
532
|
const d = db.transactions.add("the quick brown fox");
|
|
531
|
-
expect(db.indexes.docsByKeyword.find("quick")).toEqual([d]);
|
|
532
|
-
expect(db.indexes.docsByKeyword.find("brown")).toEqual([d]);
|
|
533
|
-
expect(db.indexes.docsByKeyword.find("missing")).toEqual([]);
|
|
533
|
+
expect(db.indexes.docsByKeyword.find({ keyword: "quick" })).toEqual([d]);
|
|
534
|
+
expect(db.indexes.docsByKeyword.find({ keyword: "brown" })).toEqual([d]);
|
|
535
|
+
expect(db.indexes.docsByKeyword.find({ keyword: "missing" })).toEqual([]);
|
|
534
536
|
});
|
|
535
537
|
it("findRange supports operator filters on the scalar element", () => {
|
|
536
538
|
const db = Database.create(plugin());
|
|
537
539
|
const d = db.transactions.add("alpha beta gamma");
|
|
538
|
-
const inRange = [...db.indexes.docsByKeyword.findRange({ ">=": "b", "<": "z" })].sort();
|
|
540
|
+
const inRange = [...db.indexes.docsByKeyword.findRange({ keyword: { ">=": "b", "<": "z" } })].sort();
|
|
539
541
|
expect(inRange).toEqual([d]);
|
|
540
542
|
});
|
|
541
543
|
});
|
|
@@ -556,8 +558,8 @@ describe("Pattern 8 — compound from nested data (playerByRoster)", () => {
|
|
|
556
558
|
indexes: {
|
|
557
559
|
playerByRoster: {
|
|
558
560
|
key: {
|
|
559
|
-
team: (
|
|
560
|
-
position: (
|
|
561
|
+
team: (c) => c.roster.team,
|
|
562
|
+
position: (c) => c.roster.position,
|
|
561
563
|
},
|
|
562
564
|
unique: true,
|
|
563
565
|
components: ["roster"],
|
|
@@ -593,7 +595,7 @@ describe("Pattern 10 — mixed identity + derived slots (playerByTeamRole)", ()
|
|
|
593
595
|
playerByTeamRole: {
|
|
594
596
|
key: {
|
|
595
597
|
team: "team",
|
|
596
|
-
role: (
|
|
598
|
+
role: (c) => c.roster.role,
|
|
597
599
|
},
|
|
598
600
|
unique: true,
|
|
599
601
|
components: ["roster"],
|
|
@@ -649,12 +651,135 @@ describe("Pattern 12 — custom comparator (tasksByPriority)", () => {
|
|
|
649
651
|
const e2a = db.transactions.add({ owner: 1, priority: 2, due: 10 });
|
|
650
652
|
const e2b = db.transactions.add({ owner: 1, priority: 2, due: 20 });
|
|
651
653
|
// Priority desc, then due asc.
|
|
652
|
-
expect(db.indexes.tasksByPriority.find(1)).toEqual([e3, e2a, e2b, e1]);
|
|
654
|
+
expect(db.indexes.tasksByPriority.find({ owner: 1 })).toEqual([e3, e2a, e2b, e1]);
|
|
653
655
|
});
|
|
654
656
|
});
|
|
655
657
|
// ============================================================================
|
|
656
658
|
// Cross-cutting concerns
|
|
657
659
|
// ============================================================================
|
|
660
|
+
describe("string ordering is by code point, never locale", () => {
|
|
661
|
+
const plugin = () => Database.Plugin.create({
|
|
662
|
+
components: { group: { type: "number" }, label: { type: "string" } },
|
|
663
|
+
archetypes: { Row: ["group", "label"] },
|
|
664
|
+
indexes: { byGroupSorted: { key: "group", order: { by: ["label"] } } },
|
|
665
|
+
transactions: {
|
|
666
|
+
add: (t, a) => t.archetypes.Row.insert(a),
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
it("default comparator sorts strings by ASCII code point (uppercase before lowercase)", () => {
|
|
670
|
+
const db = Database.create(plugin());
|
|
671
|
+
const a = db.transactions.add({ group: 1, label: "a" }); // 'a' = 97
|
|
672
|
+
const Z = db.transactions.add({ group: 1, label: "Z" }); // 'Z' = 90
|
|
673
|
+
const zero = db.transactions.add({ group: 1, label: "0" }); // '0' = 48
|
|
674
|
+
const A = db.transactions.add({ group: 1, label: "A" }); // 'A' = 65
|
|
675
|
+
// Code point: '0' < 'A' < 'Z' < 'a'. A locale-aware comparator would
|
|
676
|
+
// interleave case (lowercase 'a' before 'Z'), so this fails if the
|
|
677
|
+
// index ever sorts via localeCompare — the no-locale guard.
|
|
678
|
+
expect(db.indexes.byGroupSorted.find({ group: 1 })).toEqual([zero, A, Z, a]);
|
|
679
|
+
});
|
|
680
|
+
it("ordered db.select sorts strings by code point (not numeric subtraction, not locale)", () => {
|
|
681
|
+
const db = Database.create(plugin());
|
|
682
|
+
const a = db.transactions.add({ group: 1, label: "a" });
|
|
683
|
+
const Z = db.transactions.add({ group: 1, label: "Z" });
|
|
684
|
+
const zero = db.transactions.add({ group: 1, label: "0" });
|
|
685
|
+
const A = db.transactions.add({ group: 1, label: "A" });
|
|
686
|
+
// No `where`, so this hits the archetype-scan sort in selectEntities —
|
|
687
|
+
// which previously used `a - b` (NaN for strings). Now code-point.
|
|
688
|
+
expect(db.select(["label"], { order: { label: true } })).toEqual([zero, A, Z, a]);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
describe("archetype-scoped index", () => {
|
|
692
|
+
// Task and Note both have `parent`; the index is scoped to Task only.
|
|
693
|
+
const plugin = () => Database.Plugin.create({
|
|
694
|
+
components: {
|
|
695
|
+
parent: { type: "number" },
|
|
696
|
+
priority: { type: "number" },
|
|
697
|
+
body: { type: "string" },
|
|
698
|
+
},
|
|
699
|
+
archetypes: {
|
|
700
|
+
Task: ["parent", "priority"],
|
|
701
|
+
Note: ["parent", "body"],
|
|
702
|
+
},
|
|
703
|
+
indexes: {
|
|
704
|
+
tasksByParent: { key: "parent", archetype: "Task" },
|
|
705
|
+
},
|
|
706
|
+
transactions: {
|
|
707
|
+
addTask: (t, a) => t.archetypes.Task.insert(a),
|
|
708
|
+
addNote: (t, a) => t.archetypes.Note.insert(a),
|
|
709
|
+
delete: (t, e) => t.delete(e),
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
it("indexes only the scoped archetype, excluding others that share the key column", () => {
|
|
713
|
+
const db = Database.create(plugin());
|
|
714
|
+
const t1 = db.transactions.addTask({ parent: 7, priority: 1 });
|
|
715
|
+
const t2 = db.transactions.addTask({ parent: 7, priority: 2 });
|
|
716
|
+
db.transactions.addNote({ parent: 7, body: "shares parent 7 but is a Note" });
|
|
717
|
+
// Without scoping the Note (parent === 7) would appear here too.
|
|
718
|
+
expect([...db.indexes.tasksByParent.find({ parent: 7 })].sort()).toEqual([t1, t2].sort());
|
|
719
|
+
});
|
|
720
|
+
it("seeds from only the scoped archetype when registered after data exists", () => {
|
|
721
|
+
const base = Database.Plugin.create({
|
|
722
|
+
components: {
|
|
723
|
+
parent: { type: "number" },
|
|
724
|
+
priority: { type: "number" },
|
|
725
|
+
body: { type: "string" },
|
|
726
|
+
},
|
|
727
|
+
archetypes: { Task: ["parent", "priority"], Note: ["parent", "body"] },
|
|
728
|
+
transactions: {
|
|
729
|
+
addTask: (t, a) => t.archetypes.Task.insert(a),
|
|
730
|
+
addNote: (t, a) => t.archetypes.Note.insert(a),
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const db = Database.create(base);
|
|
734
|
+
const t = db.transactions.addTask({ parent: 5, priority: 1 });
|
|
735
|
+
db.transactions.addNote({ parent: 5, body: "note" });
|
|
736
|
+
const ext = db.extend(Database.Plugin.create({
|
|
737
|
+
extends: base,
|
|
738
|
+
indexes: { tasksByParent: { key: "parent", archetype: "Task" } },
|
|
739
|
+
}));
|
|
740
|
+
expect(ext.indexes.tasksByParent.find({ parent: 5 })).toEqual([t]);
|
|
741
|
+
});
|
|
742
|
+
it("throws when scoped to an unknown archetype", () => {
|
|
743
|
+
const bad = Database.Plugin.create({
|
|
744
|
+
components: { parent: { type: "number" } },
|
|
745
|
+
archetypes: { Task: ["parent"] },
|
|
746
|
+
// `archetype` typed loosely here via `as any` to reach the runtime guard.
|
|
747
|
+
indexes: { x: { key: "parent", archetype: "Nonexistent" } },
|
|
748
|
+
});
|
|
749
|
+
expect(() => Database.create(bad)).toThrow(/unknown archetype/i);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
describe("archetype-changing update keeps indexes correct (dispatch union)", () => {
|
|
753
|
+
// `byTag` only applies to archetypes that have `tag`. Adding/removing the
|
|
754
|
+
// `tag` component moves the entity between archetypes, so the entity must
|
|
755
|
+
// enter / leave the index accordingly — this exercises the registry's
|
|
756
|
+
// union(from, to) dispatch for updates that change archetype.
|
|
757
|
+
const plugin = () => Database.Plugin.create({
|
|
758
|
+
components: { tag: { type: "string" }, name: { type: "string" } },
|
|
759
|
+
archetypes: { Tagged: ["tag", "name"], Named: ["name"] },
|
|
760
|
+
indexes: { byTag: { key: "tag" } },
|
|
761
|
+
transactions: {
|
|
762
|
+
addTagged: (t, a) => t.archetypes.Tagged.insert(a),
|
|
763
|
+
addNamed: (t, a) => t.archetypes.Named.insert(a),
|
|
764
|
+
setTag: (t, a) => t.update(a.entity, { tag: a.tag }),
|
|
765
|
+
removeTag: (t, e) => t.update(e, { tag: undefined }),
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
it("removes the entity from the index when an update drops the indexed component", () => {
|
|
769
|
+
const db = Database.create(plugin());
|
|
770
|
+
const e = db.transactions.addTagged({ tag: "x", name: "n" });
|
|
771
|
+
expect(db.indexes.byTag.find({ tag: "x" })).toEqual([e]);
|
|
772
|
+
db.transactions.removeTag(e); // Tagged -> Named: byTag no longer applies
|
|
773
|
+
expect(db.indexes.byTag.find({ tag: "x" })).toEqual([]);
|
|
774
|
+
});
|
|
775
|
+
it("adds the entity to the index when an update introduces the indexed component", () => {
|
|
776
|
+
const db = Database.create(plugin());
|
|
777
|
+
const e = db.transactions.addNamed({ name: "n" });
|
|
778
|
+
expect(db.indexes.byTag.find({ tag: "y" })).toEqual([]);
|
|
779
|
+
db.transactions.setTag({ entity: e, tag: "y" }); // Named -> Tagged: enters byTag
|
|
780
|
+
expect(db.indexes.byTag.find({ tag: "y" })).toEqual([e]);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
658
783
|
describe("findRange — operator filters on the bucket key", () => {
|
|
659
784
|
it("filters by range operators on a tuple-keyed index", () => {
|
|
660
785
|
const plugin = Database.Plugin.create({
|
|
@@ -741,7 +866,7 @@ describe("auto-routing of db.select", () => {
|
|
|
741
866
|
archetypes: { U: ["name"] },
|
|
742
867
|
indexes: {
|
|
743
868
|
byLowerName: {
|
|
744
|
-
key: (
|
|
869
|
+
key: { name: (c) => c.name.toLowerCase() },
|
|
745
870
|
components: ["name"],
|
|
746
871
|
},
|
|
747
872
|
},
|
|
@@ -963,21 +1088,21 @@ describe("t.indexes — eager maintenance inside transactions", () => {
|
|
|
963
1088
|
transactions: {
|
|
964
1089
|
add: (t, args) => t.archetypes.User.insert(args),
|
|
965
1090
|
addIfNew: (t, args) => {
|
|
966
|
-
const existing = t.indexes.uniqueByEmail.get(args.email);
|
|
1091
|
+
const existing = t.indexes.uniqueByEmail.get({ email: args.email });
|
|
967
1092
|
if (existing !== null)
|
|
968
1093
|
return existing;
|
|
969
1094
|
return t.archetypes.User.insert(args);
|
|
970
1095
|
},
|
|
971
1096
|
renameAndLookup: (t, args) => {
|
|
972
1097
|
t.update(args.entity, { name: args.newName });
|
|
973
|
-
const found = t.indexes.byName.find(args.newName);
|
|
1098
|
+
const found = t.indexes.byName.find({ name: args.newName });
|
|
974
1099
|
if (!found.includes(args.entity)) {
|
|
975
1100
|
throw new Error("name index stale after t.update");
|
|
976
1101
|
}
|
|
977
1102
|
},
|
|
978
1103
|
deleteAndLookup: (t, args) => {
|
|
979
1104
|
t.delete(args.entity);
|
|
980
|
-
const stillThere = t.indexes.byName.find(args.name);
|
|
1105
|
+
const stillThere = t.indexes.byName.find({ name: args.name });
|
|
981
1106
|
if (stillThere.includes(args.entity)) {
|
|
982
1107
|
throw new Error("index still references deleted entity");
|
|
983
1108
|
}
|
|
@@ -996,14 +1121,14 @@ describe("t.indexes — eager maintenance inside transactions", () => {
|
|
|
996
1121
|
const db = Database.create(plugin());
|
|
997
1122
|
const e = db.transactions.add({ name: "alice", email: "a@a.com" });
|
|
998
1123
|
expect(() => db.transactions.renameAndLookup({ entity: e, newName: "alex" })).not.toThrow();
|
|
999
|
-
expect(db.indexes.byName.find("alex")).toEqual([e]);
|
|
1000
|
-
expect(db.indexes.byName.find("alice")).toEqual([]);
|
|
1124
|
+
expect(db.indexes.byName.find({ name: "alex" })).toEqual([e]);
|
|
1125
|
+
expect(db.indexes.byName.find({ name: "alice" })).toEqual([]);
|
|
1001
1126
|
});
|
|
1002
1127
|
it("find does not see a row deleted earlier in the same transaction body", () => {
|
|
1003
1128
|
const db = Database.create(plugin());
|
|
1004
1129
|
const e = db.transactions.add({ name: "alice", email: "a@a.com" });
|
|
1005
1130
|
expect(() => db.transactions.deleteAndLookup({ entity: e, name: "alice" })).not.toThrow();
|
|
1006
|
-
expect(db.indexes.byName.find("alice")).toEqual([]);
|
|
1131
|
+
expect(db.indexes.byName.find({ name: "alice" })).toEqual([]);
|
|
1007
1132
|
});
|
|
1008
1133
|
it("unique conflict on insert is caught up-front — no partial store or index mutation", () => {
|
|
1009
1134
|
const db = Database.create(plugin());
|
|
@@ -1012,11 +1137,11 @@ describe("t.indexes — eager maintenance inside transactions", () => {
|
|
|
1012
1137
|
const beforeRows = db.select(["email"]);
|
|
1013
1138
|
expect(() => db.transactions.add({ name: "alex", email: "shared@x.com" })).toThrow(/Unique index conflict/);
|
|
1014
1139
|
// The unique-key bucket still points at the original entity.
|
|
1015
|
-
expect(db.indexes.uniqueByEmail.get("shared@x.com")).toBe(first);
|
|
1140
|
+
expect(db.indexes.uniqueByEmail.get({ email: "shared@x.com" })).toBe(first);
|
|
1016
1141
|
// No phantom row landed in either index or store. byName.find("alex")
|
|
1017
1142
|
// returning [] proves the secondary index never saw the row;
|
|
1018
1143
|
// db.select asserts the store itself never grew.
|
|
1019
|
-
expect(db.indexes.byName.find("alex")).toEqual([]);
|
|
1144
|
+
expect(db.indexes.byName.find({ name: "alex" })).toEqual([]);
|
|
1020
1145
|
const afterRows = db.select(["email"]);
|
|
1021
1146
|
expect([...afterRows].sort()).toEqual([...beforeRows].sort());
|
|
1022
1147
|
});
|
|
@@ -1029,8 +1154,8 @@ describe("t.indexes — eager maintenance inside transactions", () => {
|
|
|
1029
1154
|
const e2BeforeRead = db.read(e2);
|
|
1030
1155
|
expect(() => db.transactions.updateEmail({ entity: e1, newEmail: "b@b.com" })).toThrow(/Unique index conflict/);
|
|
1031
1156
|
// Index untouched on both keys.
|
|
1032
|
-
expect(db.indexes.uniqueByEmail.get("a@a.com")).toBe(e1);
|
|
1033
|
-
expect(db.indexes.uniqueByEmail.get("b@b.com")).toBe(e2);
|
|
1157
|
+
expect(db.indexes.uniqueByEmail.get({ email: "a@a.com" })).toBe(e1);
|
|
1158
|
+
expect(db.indexes.uniqueByEmail.get({ email: "b@b.com" })).toBe(e2);
|
|
1034
1159
|
// Underlying store untouched — e1's email is still "a@a.com",
|
|
1035
1160
|
// proving the pre-check fired before `core.update` ran.
|
|
1036
1161
|
expect(db.read(e1)).toEqual(e1BeforeRead);
|
|
@@ -1136,7 +1261,7 @@ describe("registry maintenance", () => {
|
|
|
1136
1261
|
const e1 = db.transactions.add("alice");
|
|
1137
1262
|
const e2 = db.transactions.add("alice");
|
|
1138
1263
|
const ext = db.extend(indexed);
|
|
1139
|
-
expect([...ext.indexes.byName.find("alice")].sort()).toEqual([e1, e2].sort());
|
|
1264
|
+
expect([...ext.indexes.byName.find({ name: "alice" })].sort()).toEqual([e1, e2].sort());
|
|
1140
1265
|
});
|
|
1141
1266
|
});
|
|
1142
1267
|
//# sourceMappingURL=database.index.test.js.map
|