@dxos/index-core 0.0.0
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/LICENSE +8 -0
- package/README.md +21 -0
- package/package.json +46 -0
- package/src/index-engine.test.ts +242 -0
- package/src/index-engine.ts +187 -0
- package/src/index-tracker.test.ts +72 -0
- package/src/index-tracker.ts +104 -0
- package/src/index.ts +10 -0
- package/src/indexes/fts-index.test.ts +564 -0
- package/src/indexes/fts-index.ts +193 -0
- package/src/indexes/fts5.test.ts +27 -0
- package/src/indexes/index.ts +8 -0
- package/src/indexes/interface.ts +56 -0
- package/src/indexes/object-meta-index.test.ts +119 -0
- package/src/indexes/object-meta-index.ts +204 -0
- package/src/indexes/reverse-ref-index.test.ts +251 -0
- package/src/indexes/reverse-ref-index.ts +127 -0
- package/src/utils.ts +49 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Reactivity from '@effect/experimental/Reactivity';
|
|
6
|
+
import * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import * as SqliteClient from '@effect/sql-sqlite-node/SqliteClient';
|
|
8
|
+
import { describe, expect, it } from '@effect/vitest';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Layer from 'effect/Layer';
|
|
11
|
+
|
|
12
|
+
import { ATTR_TYPE } from '@dxos/echo/internal';
|
|
13
|
+
import { DXN, ObjectId, SpaceId } from '@dxos/keys';
|
|
14
|
+
|
|
15
|
+
import { FtsIndex } from './fts-index';
|
|
16
|
+
import type { IndexerObject } from './interface';
|
|
17
|
+
import { ObjectMetaIndex } from './object-meta-index';
|
|
18
|
+
|
|
19
|
+
const TYPE_PERSON = DXN.parse('dxn:type:example.com/type/Person:0.1.0').toString();
|
|
20
|
+
const TYPE_DEFAULT = DXN.parse('dxn:type:test.com/type/Type:0.1.0').toString();
|
|
21
|
+
|
|
22
|
+
const TestLayer = Layer.merge(
|
|
23
|
+
SqliteClient.layer({
|
|
24
|
+
filename: ':memory:',
|
|
25
|
+
}),
|
|
26
|
+
Reactivity.layer,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
describe('FtsIndex', () => {
|
|
30
|
+
it.effect(
|
|
31
|
+
'should create an FTS5 table on migrate',
|
|
32
|
+
Effect.fnUntraced(function* () {
|
|
33
|
+
const index = new FtsIndex();
|
|
34
|
+
yield* index.migrate();
|
|
35
|
+
|
|
36
|
+
const sql = yield* SqlClient.SqlClient;
|
|
37
|
+
const result = yield* sql`SELECT sql FROM sqlite_master WHERE name = 'ftsIndex'`;
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveLength(1);
|
|
40
|
+
expect(result[0].sql).toMatch(/fts5/i);
|
|
41
|
+
expect(result[0].sql).toMatch(/snapshot/i);
|
|
42
|
+
}, Effect.provide(TestLayer)),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
it.effect(
|
|
46
|
+
'should insert snapshots and query them via MATCH',
|
|
47
|
+
Effect.fnUntraced(function* () {
|
|
48
|
+
const index = new FtsIndex();
|
|
49
|
+
const metaIndex = new ObjectMetaIndex();
|
|
50
|
+
yield* index.migrate();
|
|
51
|
+
yield* metaIndex.migrate();
|
|
52
|
+
|
|
53
|
+
const spaceId = SpaceId.random();
|
|
54
|
+
const objects: IndexerObject[] = [
|
|
55
|
+
{
|
|
56
|
+
spaceId,
|
|
57
|
+
queueId: null,
|
|
58
|
+
documentId: 'doc-1',
|
|
59
|
+
recordId: null,
|
|
60
|
+
data: {
|
|
61
|
+
id: ObjectId.random(),
|
|
62
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
63
|
+
title: 'Hello Effect',
|
|
64
|
+
body: 'This is a message about Effect and SQL.',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
yield* metaIndex.update(objects);
|
|
70
|
+
yield* metaIndex.lookupRecordIds(objects);
|
|
71
|
+
yield* index.update(objects);
|
|
72
|
+
|
|
73
|
+
const match = yield* index.query({ query: 'Effect', spaceId: null, includeAllQueues: false, queueIds: null });
|
|
74
|
+
expect(match.length).toBeGreaterThan(0);
|
|
75
|
+
expect(match[0].objectId).toBe(objects[0].data.id);
|
|
76
|
+
|
|
77
|
+
const noMatch = yield* index.query({
|
|
78
|
+
query: 'DefinitelyNotPresent',
|
|
79
|
+
spaceId: null,
|
|
80
|
+
includeAllQueues: false,
|
|
81
|
+
queueIds: null,
|
|
82
|
+
});
|
|
83
|
+
expect(noMatch).toHaveLength(0);
|
|
84
|
+
}, Effect.provide(TestLayer)),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it.effect(
|
|
88
|
+
'should upsert objects on update',
|
|
89
|
+
Effect.fnUntraced(function* () {
|
|
90
|
+
const index = new FtsIndex();
|
|
91
|
+
const metaIndex = new ObjectMetaIndex();
|
|
92
|
+
yield* index.migrate();
|
|
93
|
+
yield* metaIndex.migrate();
|
|
94
|
+
|
|
95
|
+
const spaceId = SpaceId.random();
|
|
96
|
+
const objectId = ObjectId.random();
|
|
97
|
+
|
|
98
|
+
// Initial insert.
|
|
99
|
+
const obj1: IndexerObject = {
|
|
100
|
+
spaceId,
|
|
101
|
+
queueId: null,
|
|
102
|
+
documentId: 'doc-1',
|
|
103
|
+
recordId: null,
|
|
104
|
+
data: {
|
|
105
|
+
id: objectId,
|
|
106
|
+
[ATTR_TYPE]: DXN.parse('dxn:type:example.com/type/Person:0.1.0').toString(),
|
|
107
|
+
title: 'Original Title',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
yield* metaIndex.update([obj1]);
|
|
111
|
+
yield* metaIndex.lookupRecordIds([obj1]);
|
|
112
|
+
yield* index.update([obj1]);
|
|
113
|
+
|
|
114
|
+
let match = yield* index.query({ query: 'Original', spaceId: null, includeAllQueues: false, queueIds: null });
|
|
115
|
+
expect(match.length).toBe(1);
|
|
116
|
+
|
|
117
|
+
// Update with same doc id and object id.
|
|
118
|
+
const obj2: IndexerObject = {
|
|
119
|
+
spaceId,
|
|
120
|
+
queueId: null,
|
|
121
|
+
documentId: 'doc-1',
|
|
122
|
+
recordId: null,
|
|
123
|
+
data: {
|
|
124
|
+
id: objectId,
|
|
125
|
+
[ATTR_TYPE]: TYPE_DEFAULT,
|
|
126
|
+
title: 'Updated Title',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
// Meta index update is required if metadata changed, but for FTS query to work, we just need the join to succeed.
|
|
130
|
+
// recordId is persistent.
|
|
131
|
+
yield* metaIndex.update([obj2]);
|
|
132
|
+
yield* metaIndex.lookupRecordIds([obj2]);
|
|
133
|
+
yield* index.update([obj2]);
|
|
134
|
+
|
|
135
|
+
// Old content should be gone.
|
|
136
|
+
match = yield* index.query({ query: 'Original', spaceId: null, includeAllQueues: false, queueIds: null });
|
|
137
|
+
expect(match.length).toBe(0);
|
|
138
|
+
|
|
139
|
+
// New content should exist.
|
|
140
|
+
match = yield* index.query({ query: 'Updated', spaceId: null, includeAllQueues: false, queueIds: null });
|
|
141
|
+
expect(match.length).toBe(1);
|
|
142
|
+
}, Effect.provide(TestLayer)),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
it.effect(
|
|
146
|
+
'should handle non-sequential recordIds',
|
|
147
|
+
Effect.fnUntraced(function* () {
|
|
148
|
+
const index = new FtsIndex();
|
|
149
|
+
const metaIndex = new ObjectMetaIndex();
|
|
150
|
+
yield* index.migrate();
|
|
151
|
+
yield* metaIndex.migrate();
|
|
152
|
+
|
|
153
|
+
const spaceId = SpaceId.random();
|
|
154
|
+
const objects: IndexerObject[] = [
|
|
155
|
+
{
|
|
156
|
+
spaceId,
|
|
157
|
+
queueId: null,
|
|
158
|
+
documentId: 'doc-100',
|
|
159
|
+
recordId: null,
|
|
160
|
+
data: {
|
|
161
|
+
id: ObjectId.random(),
|
|
162
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
163
|
+
title: 'Alpha Document',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
spaceId,
|
|
168
|
+
queueId: null,
|
|
169
|
+
documentId: 'doc-200',
|
|
170
|
+
recordId: null,
|
|
171
|
+
data: {
|
|
172
|
+
id: ObjectId.random(),
|
|
173
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
174
|
+
title: 'Beta Document',
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
spaceId,
|
|
179
|
+
queueId: null,
|
|
180
|
+
documentId: 'doc-1000',
|
|
181
|
+
recordId: null,
|
|
182
|
+
data: {
|
|
183
|
+
id: ObjectId.random(),
|
|
184
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
185
|
+
title: 'Gamma Document',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
yield* metaIndex.update(objects);
|
|
191
|
+
yield* metaIndex.lookupRecordIds(objects);
|
|
192
|
+
yield* index.update(objects);
|
|
193
|
+
|
|
194
|
+
// All documents should be queryable.
|
|
195
|
+
const alphaMatch = yield* index.query({
|
|
196
|
+
query: 'Alpha',
|
|
197
|
+
spaceId: null,
|
|
198
|
+
includeAllQueues: false,
|
|
199
|
+
queueIds: null,
|
|
200
|
+
});
|
|
201
|
+
expect(alphaMatch).toHaveLength(1);
|
|
202
|
+
|
|
203
|
+
// Query that matches all.
|
|
204
|
+
const allMatch = yield* index.query({
|
|
205
|
+
query: 'Document',
|
|
206
|
+
spaceId: null,
|
|
207
|
+
includeAllQueues: false,
|
|
208
|
+
queueIds: null,
|
|
209
|
+
});
|
|
210
|
+
expect(allMatch).toHaveLength(3);
|
|
211
|
+
}, Effect.provide(TestLayer)),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
it.effect(
|
|
215
|
+
'should query from one space only',
|
|
216
|
+
Effect.fnUntraced(function* () {
|
|
217
|
+
const index = new FtsIndex();
|
|
218
|
+
const metaIndex = new ObjectMetaIndex();
|
|
219
|
+
yield* index.migrate();
|
|
220
|
+
yield* metaIndex.migrate();
|
|
221
|
+
|
|
222
|
+
const space1 = SpaceId.random();
|
|
223
|
+
const space2 = SpaceId.random();
|
|
224
|
+
|
|
225
|
+
const obj1: IndexerObject = {
|
|
226
|
+
spaceId: space1,
|
|
227
|
+
queueId: null,
|
|
228
|
+
documentId: 'doc-s1',
|
|
229
|
+
recordId: null,
|
|
230
|
+
data: {
|
|
231
|
+
id: ObjectId.random(),
|
|
232
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
233
|
+
title: 'Space One Content',
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const obj2: IndexerObject = {
|
|
238
|
+
spaceId: space2,
|
|
239
|
+
queueId: null,
|
|
240
|
+
documentId: 'doc-s2',
|
|
241
|
+
recordId: null,
|
|
242
|
+
data: {
|
|
243
|
+
id: ObjectId.random(),
|
|
244
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
245
|
+
title: 'Space Two Content',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
yield* metaIndex.update([obj1, obj2]);
|
|
250
|
+
yield* metaIndex.lookupRecordIds([obj1, obj2]);
|
|
251
|
+
yield* index.update([obj1, obj2]);
|
|
252
|
+
|
|
253
|
+
// Query without spaceId should return both (if term matches both) or specific one
|
|
254
|
+
// Let's search for "Content" which is in both
|
|
255
|
+
const allMatches = yield* index.query({
|
|
256
|
+
query: 'Content',
|
|
257
|
+
spaceId: null,
|
|
258
|
+
includeAllQueues: false,
|
|
259
|
+
queueIds: null,
|
|
260
|
+
});
|
|
261
|
+
expect(allMatches).toHaveLength(2);
|
|
262
|
+
|
|
263
|
+
// Query space 1
|
|
264
|
+
const s1Matches = yield* index.query({
|
|
265
|
+
query: 'Content',
|
|
266
|
+
spaceId: [space1],
|
|
267
|
+
includeAllQueues: false,
|
|
268
|
+
queueIds: null,
|
|
269
|
+
});
|
|
270
|
+
expect(s1Matches).toHaveLength(1);
|
|
271
|
+
expect(s1Matches[0].objectId).toBe(obj1.data.id);
|
|
272
|
+
|
|
273
|
+
// Query space 2
|
|
274
|
+
const s2Matches = yield* index.query({
|
|
275
|
+
query: 'Content',
|
|
276
|
+
spaceId: [space2],
|
|
277
|
+
includeAllQueues: false,
|
|
278
|
+
queueIds: null,
|
|
279
|
+
});
|
|
280
|
+
expect(s2Matches).toHaveLength(1);
|
|
281
|
+
expect(s2Matches[0].objectId).toBe(obj2.data.id);
|
|
282
|
+
}, Effect.provide(TestLayer)),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
it.effect(
|
|
286
|
+
'partial word matches',
|
|
287
|
+
Effect.fnUntraced(function* () {
|
|
288
|
+
const index = new FtsIndex();
|
|
289
|
+
const metaIndex = new ObjectMetaIndex();
|
|
290
|
+
yield* index.migrate();
|
|
291
|
+
yield* metaIndex.migrate();
|
|
292
|
+
|
|
293
|
+
const spaceId = SpaceId.random();
|
|
294
|
+
const objects: IndexerObject[] = [
|
|
295
|
+
{
|
|
296
|
+
spaceId,
|
|
297
|
+
queueId: null,
|
|
298
|
+
documentId: 'doc-1',
|
|
299
|
+
recordId: null,
|
|
300
|
+
data: {
|
|
301
|
+
id: ObjectId.random(),
|
|
302
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
303
|
+
title: 'Programming in TypeScript',
|
|
304
|
+
body: 'Learn about functional programming patterns.',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
spaceId,
|
|
309
|
+
queueId: null,
|
|
310
|
+
documentId: 'doc-2',
|
|
311
|
+
recordId: null,
|
|
312
|
+
data: {
|
|
313
|
+
id: ObjectId.random(),
|
|
314
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
315
|
+
title: 'Database Design',
|
|
316
|
+
body: 'Understanding program architecture.',
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
yield* metaIndex.update(objects);
|
|
322
|
+
yield* metaIndex.lookupRecordIds(objects);
|
|
323
|
+
yield* index.update(objects);
|
|
324
|
+
|
|
325
|
+
const defaultQuery = { spaceId: null, includeAllQueues: false, queueIds: null } as const;
|
|
326
|
+
|
|
327
|
+
// Full word matches exactly.
|
|
328
|
+
const exactMatch = yield* index.query({ query: 'Programming', ...defaultQuery });
|
|
329
|
+
expect(exactMatch).toHaveLength(1);
|
|
330
|
+
expect(exactMatch[0].objectId).toBe(objects[0].data.id);
|
|
331
|
+
|
|
332
|
+
// Empty query should return no results.
|
|
333
|
+
const emptyMatch = yield* index.query({ query: '', ...defaultQuery });
|
|
334
|
+
expect(emptyMatch).toHaveLength(0);
|
|
335
|
+
|
|
336
|
+
// Single character query should return all results.
|
|
337
|
+
const singleCharMatch = yield* index.query({ query: 'P', ...defaultQuery });
|
|
338
|
+
expect(singleCharMatch).toHaveLength(2);
|
|
339
|
+
|
|
340
|
+
// Trigram tokenizer enables substring matching - partial words match.
|
|
341
|
+
const partialMatch = yield* index.query({ query: 'Prog', ...defaultQuery });
|
|
342
|
+
expect(partialMatch).toHaveLength(2);
|
|
343
|
+
|
|
344
|
+
// Substring in the middle of a word matches (trigram).
|
|
345
|
+
const substringMatch = yield* index.query({ query: 'rog', ...defaultQuery });
|
|
346
|
+
expect(substringMatch).toHaveLength(2); // "Programming" and "program" both contain "rog".
|
|
347
|
+
|
|
348
|
+
// Multiple words query matches documents containing all substrings.
|
|
349
|
+
const multiWord = yield* index.query({ query: 'program architecture', ...defaultQuery });
|
|
350
|
+
expect(multiWord).toHaveLength(1);
|
|
351
|
+
expect(multiWord[0].objectId).toBe(objects[1].data.id);
|
|
352
|
+
|
|
353
|
+
// Wrong order of words still matches (implicit AND).
|
|
354
|
+
const wrongOrderMatch = yield* index.query({ query: 'architecture program', ...defaultQuery });
|
|
355
|
+
expect(wrongOrderMatch).toHaveLength(1);
|
|
356
|
+
|
|
357
|
+
// Phrase query with double quotes for exact sequence.
|
|
358
|
+
const phraseMatch = yield* index.query({ query: 'functional programming', ...defaultQuery });
|
|
359
|
+
expect(phraseMatch).toHaveLength(1);
|
|
360
|
+
expect(phraseMatch[0].objectId).toBe(objects[0].data.id);
|
|
361
|
+
}, Effect.provide(TestLayer)),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
it.effect(
|
|
365
|
+
'should query from specific queues',
|
|
366
|
+
Effect.fnUntraced(function* () {
|
|
367
|
+
const index = new FtsIndex();
|
|
368
|
+
const metaIndex = new ObjectMetaIndex();
|
|
369
|
+
yield* index.migrate();
|
|
370
|
+
yield* metaIndex.migrate();
|
|
371
|
+
|
|
372
|
+
const spaceId = SpaceId.random();
|
|
373
|
+
const queue1 = ObjectId.random();
|
|
374
|
+
const queue2 = ObjectId.random();
|
|
375
|
+
|
|
376
|
+
const spaceObj: IndexerObject = {
|
|
377
|
+
spaceId,
|
|
378
|
+
queueId: null,
|
|
379
|
+
documentId: 'doc-space',
|
|
380
|
+
recordId: null,
|
|
381
|
+
data: {
|
|
382
|
+
id: ObjectId.random(),
|
|
383
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
384
|
+
title: 'Space Content',
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const queue1Obj: IndexerObject = {
|
|
389
|
+
spaceId,
|
|
390
|
+
queueId: queue1,
|
|
391
|
+
documentId: null,
|
|
392
|
+
recordId: null,
|
|
393
|
+
data: {
|
|
394
|
+
id: ObjectId.random(),
|
|
395
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
396
|
+
title: 'Queue One Content',
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const queue2Obj: IndexerObject = {
|
|
401
|
+
spaceId,
|
|
402
|
+
queueId: queue2,
|
|
403
|
+
documentId: null,
|
|
404
|
+
recordId: null,
|
|
405
|
+
data: {
|
|
406
|
+
id: ObjectId.random(),
|
|
407
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
408
|
+
title: 'Queue Two Content',
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
yield* metaIndex.update([spaceObj, queue1Obj, queue2Obj]);
|
|
413
|
+
yield* metaIndex.lookupRecordIds([spaceObj, queue1Obj, queue2Obj]);
|
|
414
|
+
yield* index.update([spaceObj, queue1Obj, queue2Obj]);
|
|
415
|
+
|
|
416
|
+
// Query specific queue only.
|
|
417
|
+
const q1Matches = yield* index.query({
|
|
418
|
+
query: 'Content',
|
|
419
|
+
spaceId: null,
|
|
420
|
+
includeAllQueues: false,
|
|
421
|
+
queueIds: [queue1],
|
|
422
|
+
});
|
|
423
|
+
expect(q1Matches).toHaveLength(1);
|
|
424
|
+
expect(q1Matches[0].objectId).toBe(queue1Obj.data.id);
|
|
425
|
+
|
|
426
|
+
// Query multiple queues.
|
|
427
|
+
const bothQueuesMatches = yield* index.query({
|
|
428
|
+
query: 'Content',
|
|
429
|
+
spaceId: null,
|
|
430
|
+
includeAllQueues: false,
|
|
431
|
+
queueIds: [queue1, queue2],
|
|
432
|
+
});
|
|
433
|
+
expect(bothQueuesMatches).toHaveLength(2);
|
|
434
|
+
}, Effect.provide(TestLayer)),
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
it.effect(
|
|
438
|
+
'should query with includeAllQueues',
|
|
439
|
+
Effect.fnUntraced(function* () {
|
|
440
|
+
const index = new FtsIndex();
|
|
441
|
+
const metaIndex = new ObjectMetaIndex();
|
|
442
|
+
yield* index.migrate();
|
|
443
|
+
yield* metaIndex.migrate();
|
|
444
|
+
|
|
445
|
+
const spaceId = SpaceId.random();
|
|
446
|
+
const queueId = ObjectId.random();
|
|
447
|
+
|
|
448
|
+
const spaceObj: IndexerObject = {
|
|
449
|
+
spaceId,
|
|
450
|
+
queueId: null,
|
|
451
|
+
documentId: 'doc-space',
|
|
452
|
+
recordId: null,
|
|
453
|
+
data: {
|
|
454
|
+
id: ObjectId.random(),
|
|
455
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
456
|
+
title: 'Space Content',
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const queueObj: IndexerObject = {
|
|
461
|
+
spaceId,
|
|
462
|
+
queueId,
|
|
463
|
+
documentId: null,
|
|
464
|
+
recordId: null,
|
|
465
|
+
data: {
|
|
466
|
+
id: ObjectId.random(),
|
|
467
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
468
|
+
title: 'Queue Content',
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
yield* metaIndex.update([spaceObj, queueObj]);
|
|
473
|
+
yield* metaIndex.lookupRecordIds([spaceObj, queueObj]);
|
|
474
|
+
yield* index.update([spaceObj, queueObj]);
|
|
475
|
+
|
|
476
|
+
// Query space without includeAllQueues - should only return space object.
|
|
477
|
+
const spaceOnlyMatches = yield* index.query({
|
|
478
|
+
query: 'Content',
|
|
479
|
+
spaceId: [spaceId],
|
|
480
|
+
includeAllQueues: false,
|
|
481
|
+
queueIds: null,
|
|
482
|
+
});
|
|
483
|
+
expect(spaceOnlyMatches).toHaveLength(1);
|
|
484
|
+
expect(spaceOnlyMatches[0].objectId).toBe(spaceObj.data.id);
|
|
485
|
+
|
|
486
|
+
// Query space with includeAllQueues - should return both.
|
|
487
|
+
const allMatches = yield* index.query({
|
|
488
|
+
query: 'Content',
|
|
489
|
+
spaceId: [spaceId],
|
|
490
|
+
includeAllQueues: true,
|
|
491
|
+
queueIds: null,
|
|
492
|
+
});
|
|
493
|
+
expect(allMatches).toHaveLength(2);
|
|
494
|
+
}, Effect.provide(TestLayer)),
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
it.effect(
|
|
498
|
+
'should OR space and queue constraints',
|
|
499
|
+
Effect.fnUntraced(function* () {
|
|
500
|
+
const index = new FtsIndex();
|
|
501
|
+
const metaIndex = new ObjectMetaIndex();
|
|
502
|
+
yield* index.migrate();
|
|
503
|
+
yield* metaIndex.migrate();
|
|
504
|
+
|
|
505
|
+
const space1 = SpaceId.random();
|
|
506
|
+
const space2 = SpaceId.random();
|
|
507
|
+
const queueInSpace2 = ObjectId.random();
|
|
508
|
+
|
|
509
|
+
const space1Obj: IndexerObject = {
|
|
510
|
+
spaceId: space1,
|
|
511
|
+
queueId: null,
|
|
512
|
+
documentId: 'doc-s1',
|
|
513
|
+
recordId: null,
|
|
514
|
+
data: {
|
|
515
|
+
id: ObjectId.random(),
|
|
516
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
517
|
+
title: 'Space One Content',
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const space2Obj: IndexerObject = {
|
|
522
|
+
spaceId: space2,
|
|
523
|
+
queueId: null,
|
|
524
|
+
documentId: 'doc-s2',
|
|
525
|
+
recordId: null,
|
|
526
|
+
data: {
|
|
527
|
+
id: ObjectId.random(),
|
|
528
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
529
|
+
title: 'Space Two Content',
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const queueObj: IndexerObject = {
|
|
534
|
+
spaceId: space2,
|
|
535
|
+
queueId: queueInSpace2,
|
|
536
|
+
documentId: null,
|
|
537
|
+
recordId: null,
|
|
538
|
+
data: {
|
|
539
|
+
id: ObjectId.random(),
|
|
540
|
+
[ATTR_TYPE]: TYPE_PERSON,
|
|
541
|
+
title: 'Queue Content',
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
yield* metaIndex.update([space1Obj, space2Obj, queueObj]);
|
|
546
|
+
yield* metaIndex.lookupRecordIds([space1Obj, space2Obj, queueObj]);
|
|
547
|
+
yield* index.update([space1Obj, space2Obj, queueObj]);
|
|
548
|
+
|
|
549
|
+
// Query space1 OR specific queue in space2 - should return space1 object and queue object.
|
|
550
|
+
const orMatches = yield* index.query({
|
|
551
|
+
query: 'Content',
|
|
552
|
+
spaceId: [space1],
|
|
553
|
+
includeAllQueues: false,
|
|
554
|
+
queueIds: [queueInSpace2],
|
|
555
|
+
});
|
|
556
|
+
expect(orMatches).toHaveLength(2);
|
|
557
|
+
const objectIds = orMatches.map((m) => m.objectId);
|
|
558
|
+
expect(objectIds).toContain(space1Obj.data.id);
|
|
559
|
+
expect(objectIds).toContain(queueObj.data.id);
|
|
560
|
+
// Should NOT contain space2 object (not in space1 and not the specified queue).
|
|
561
|
+
expect(objectIds).not.toContain(space2Obj.data.id);
|
|
562
|
+
}, Effect.provide(TestLayer)),
|
|
563
|
+
);
|
|
564
|
+
});
|