@dxos/index-core 0.8.4-main.fcfe5033a5 → 0.9.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.
Files changed (40) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/index.mjs +190 -101
  4. package/dist/lib/neutral/index.mjs.map +4 -4
  5. package/dist/lib/neutral/meta.json +1 -1
  6. package/dist/types/src/index-engine.d.ts +31 -17
  7. package/dist/types/src/index-engine.d.ts.map +1 -1
  8. package/dist/types/src/index-tracker.d.ts +3 -3
  9. package/dist/types/src/index.d.ts +3 -3
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/indexes/entity-meta-index.d.ts +113 -0
  12. package/dist/types/src/indexes/entity-meta-index.d.ts.map +1 -0
  13. package/dist/types/src/indexes/entity-meta-index.test.d.ts +2 -0
  14. package/dist/types/src/indexes/entity-meta-index.test.d.ts.map +1 -0
  15. package/dist/types/src/indexes/fts-index.d.ts +7 -6
  16. package/dist/types/src/indexes/fts-index.d.ts.map +1 -1
  17. package/dist/types/src/indexes/index.d.ts +1 -1
  18. package/dist/types/src/indexes/interface.d.ts +15 -4
  19. package/dist/types/src/indexes/interface.d.ts.map +1 -1
  20. package/dist/types/src/indexes/reverse-ref-index.d.ts +3 -2
  21. package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -1
  22. package/dist/types/src/utils.d.ts +3 -3
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +13 -18
  25. package/src/index-engine.test.ts +138 -16
  26. package/src/index-engine.ts +123 -58
  27. package/src/index.ts +9 -3
  28. package/src/indexes/{object-meta-index.test.ts → entity-meta-index.test.ts} +114 -53
  29. package/src/indexes/{object-meta-index.ts → entity-meta-index.ts} +140 -71
  30. package/src/indexes/fts-index.test.ts +188 -34
  31. package/src/indexes/fts-index.ts +32 -15
  32. package/src/indexes/index.ts +1 -1
  33. package/src/indexes/interface.ts +16 -4
  34. package/src/indexes/reverse-ref-index.test.ts +57 -43
  35. package/src/indexes/reverse-ref-index.ts +22 -14
  36. package/src/utils.ts +5 -5
  37. package/dist/types/src/indexes/object-meta-index.d.ts +0 -88
  38. package/dist/types/src/indexes/object-meta-index.d.ts.map +0 -1
  39. package/dist/types/src/indexes/object-meta-index.test.d.ts +0 -2
  40. package/dist/types/src/indexes/object-meta-index.test.d.ts.map +0 -1
@@ -9,11 +9,28 @@ import * as Effect from 'effect/Effect';
9
9
  import * as Schema from 'effect/Schema';
10
10
 
11
11
  import { ATTR_DELETED, ATTR_PARENT, ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET, ATTR_TYPE } from '@dxos/echo/internal';
12
- import { DXN, type ObjectId, type SpaceId } from '@dxos/keys';
12
+ import { DXN, EID, EntityId, SpaceId, URI } from '@dxos/keys';
13
13
 
14
14
  import type { IndexerObject } from './interface';
15
15
  import type { Index } from './interface';
16
16
 
17
+ /**
18
+ * Normalizes an echo: EID to the local (unqualified) form so SQL comparisons are consistent.
19
+ * DB rows always store `echo:/<entityId>`; a space-qualified `echo://<space>/<entityId>` would
20
+ * otherwise miss every row for that type.
21
+ */
22
+ const _normalizeTypeUri = (typeDXN: string): string => {
23
+ if (!typeDXN.startsWith('echo:')) {
24
+ return typeDXN;
25
+ }
26
+ const eid = EID.tryParse(typeDXN);
27
+ if (!eid) {
28
+ return typeDXN;
29
+ }
30
+ const entityId = EID.getEntityId(eid);
31
+ return entityId ? EID.make({ entityId }) : typeDXN;
32
+ };
33
+
17
34
  const _escapeLikePrefix = (prefix: string) => {
18
35
  // Escape LIKE metacharacters in the *literal* prefix (we still append a wildcard for the version suffix).
19
36
  // Backslash is used as the ESCAPE character.
@@ -22,20 +39,27 @@ const _escapeLikePrefix = (prefix: string) => {
22
39
  return `${escaped}:%`;
23
40
  };
24
41
 
25
- export const ObjectMeta = Schema.Struct({
42
+ export const EntityMeta = Schema.Struct({
26
43
  recordId: Schema.Number,
27
- objectId: Schema.String,
44
+ objectId: EntityId,
45
+ /** Empty string for non-queue objects. */
28
46
  queueId: Schema.String,
29
- spaceId: Schema.String,
47
+ /** Queue subspace namespace (e.g. 'data', 'trace'). Empty string for non-queue objects. */
48
+ queueNamespace: Schema.String,
49
+ spaceId: SpaceId,
30
50
  documentId: Schema.String,
31
51
  entityKind: Schema.String,
32
- /** The versioned DXN of the type of the object. */
33
- typeDxn: Schema.String,
52
+ /**
53
+ * Type identifier URI for the object — typename DXN for non-stored schemas,
54
+ * schema-as-object EID for stored (dynamic) schemas. Mirrors the value
55
+ * written into the object's `system.type`.
56
+ */
57
+ typeDXN: URI.Schema,
34
58
  deleted: Schema.Boolean,
35
- source: Schema.NullOr(Schema.String),
36
- target: Schema.NullOr(Schema.String),
59
+ source: Schema.NullOr(EID.Schema),
60
+ target: Schema.NullOr(EID.Schema),
37
61
  /** Parent object id (nullable). */
38
- parent: Schema.NullOr(Schema.String),
62
+ parent: Schema.NullOr(EID.Schema),
39
63
  /** Monotonically increasing sequence number assigned on insert/update for tracking indexing order. */
40
64
  version: Schema.Number,
41
65
  /** Unix ms timestamp when the object was first indexed. */
@@ -43,7 +67,7 @@ export const ObjectMeta = Schema.Struct({
43
67
  /** Unix ms timestamp when the object was last re-indexed. */
44
68
  updatedAt: Schema.NullOr(Schema.Number),
45
69
  });
46
- export interface ObjectMeta extends Schema.Schema.Type<typeof ObjectMeta> {}
70
+ export interface EntityMeta extends Schema.Schema.Type<typeof EntityMeta> {}
47
71
 
48
72
  /**
49
73
  * Builds a SQL condition for filtering by space and queue source.
@@ -76,18 +100,19 @@ const buildSourceCondition = (
76
100
  return sql.or(conditions);
77
101
  };
78
102
 
79
- export class ObjectMetaIndex implements Index {
80
- migrate = Effect.fn('ObjectMetaIndex.runMigrations')(function* () {
103
+ export class EntityMetaIndex implements Index {
104
+ migrate = Effect.fn('EntityMetaIndex.runMigrations')(function* () {
81
105
  const sql = yield* SqlClient.SqlClient;
82
106
 
83
107
  yield* sql`CREATE TABLE IF NOT EXISTS objectMeta (
84
108
  recordId INTEGER PRIMARY KEY AUTOINCREMENT,
85
109
  objectId TEXT NOT NULL,
86
110
  queueId TEXT NOT NULL DEFAULT '',
111
+ queueNamespace TEXT NOT NULL DEFAULT '',
87
112
  spaceId TEXT NOT NULL,
88
113
  documentId TEXT NOT NULL DEFAULT '',
89
114
  entityKind TEXT NOT NULL,
90
- typeDxn TEXT NOT NULL,
115
+ typeDXN TEXT NOT NULL,
91
116
  deleted INTEGER NOT NULL,
92
117
  source TEXT,
93
118
  target TEXT,
@@ -102,30 +127,36 @@ export class ObjectMetaIndex implements Index {
102
127
  // Add timestamp columns for tables created before they were introduced.
103
128
  yield* Effect.catchAll(sql`ALTER TABLE objectMeta ADD COLUMN createdAt INTEGER`, () => Effect.void);
104
129
  yield* Effect.catchAll(sql`ALTER TABLE objectMeta ADD COLUMN updatedAt INTEGER`, () => Effect.void);
130
+ // Add queueNamespace column for tables created before it was introduced.
131
+ yield* Effect.catchAll(
132
+ sql`ALTER TABLE objectMeta ADD COLUMN queueNamespace TEXT NOT NULL DEFAULT ''`,
133
+ () => Effect.void,
134
+ );
105
135
 
106
136
  yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_objectId ON objectMeta(spaceId, objectId)`;
107
- yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_typeDxn ON objectMeta(spaceId, typeDxn)`;
137
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_typeDXN ON objectMeta(spaceId, typeDXN)`;
108
138
  yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_version ON objectMeta(version)`;
109
139
  yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_parent ON objectMeta(spaceId, parent)`;
110
140
  yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_updatedAt ON objectMeta(updatedAt)`;
111
141
  yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_createdAt ON objectMeta(createdAt)`;
112
142
  });
113
143
 
114
- query = Effect.fn('ObjectMetaIndex.query')(
144
+ query = Effect.fn('EntityMetaIndex.query')(
115
145
  (
116
- query: Pick<ObjectMeta, 'spaceId' | 'typeDxn'>,
117
- ): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
146
+ query: Pick<EntityMeta, 'spaceId' | 'typeDXN'>,
147
+ ): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
118
148
  Effect.gen(function* () {
119
149
  const sql = yield* SqlClient.SqlClient;
120
- const parsedType = DXN.tryParse(query.typeDxn)?.asTypeDXN();
150
+ const normalizedTypeDXN = _normalizeTypeUri(query.typeDXN);
151
+ const parsedDxn = DXN.isDXN(normalizedTypeDXN) ? normalizedTypeDXN : undefined;
152
+ const hasNoVersion = parsedDxn !== undefined && DXN.getVersion(parsedDxn) === undefined;
121
153
 
122
154
  // SQLite stores booleans as integers, so we need to specify the raw row type.
123
- const rows =
124
- parsedType && parsedType.version === undefined
125
- ? yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND (typeDxn = ${
126
- query.typeDxn
127
- } OR typeDxn LIKE ${_escapeLikePrefix(query.typeDxn)} ESCAPE '\\')`
128
- : yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND typeDxn = ${query.typeDxn}`;
155
+ const rows = hasNoVersion
156
+ ? yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND (typeDXN = ${
157
+ normalizedTypeDXN
158
+ } OR typeDXN LIKE ${_escapeLikePrefix(normalizedTypeDXN)} ESCAPE '\\')`
159
+ : yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND typeDXN = ${normalizedTypeDXN}`;
129
160
  return rows.map((row) => ({
130
161
  ...row,
131
162
  deleted: !!row.deleted,
@@ -133,12 +164,12 @@ export class ObjectMetaIndex implements Index {
133
164
  }),
134
165
  );
135
166
 
136
- queryAll = Effect.fn('ObjectMetaIndex.queryAll')(
167
+ queryAll = Effect.fn('EntityMetaIndex.queryAll')(
137
168
  (query: {
138
- spaceIds: readonly ObjectMeta['spaceId'][];
169
+ spaceIds: readonly EntityMeta['spaceId'][];
139
170
  includeAllQueues?: boolean;
140
171
  queueIds?: readonly string[] | null;
141
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
172
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
142
173
  Effect.gen(function* () {
143
174
  if (query.spaceIds.length === 0 && (!query.queueIds || query.queueIds.length === 0)) {
144
175
  return [];
@@ -151,7 +182,7 @@ export class ObjectMetaIndex implements Index {
151
182
  query.includeAllQueues ?? false,
152
183
  query.queueIds ?? null,
153
184
  );
154
- const rows = yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
185
+ const rows = yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
155
186
  return rows.map((row) => ({
156
187
  ...row,
157
188
  deleted: !!row.deleted,
@@ -159,7 +190,7 @@ export class ObjectMetaIndex implements Index {
159
190
  }),
160
191
  );
161
192
 
162
- queryTypes = Effect.fn('ObjectMetaIndex.queryTypes')(
193
+ queryTypes = Effect.fn('EntityMetaIndex.queryTypes')(
163
194
  ({
164
195
  spaceIds,
165
196
  typeDxns,
@@ -167,12 +198,12 @@ export class ObjectMetaIndex implements Index {
167
198
  includeAllQueues = false,
168
199
  queueIds = null,
169
200
  }: {
170
- spaceIds: readonly ObjectMeta['spaceId'][];
171
- typeDxns: readonly ObjectMeta['typeDxn'][];
201
+ spaceIds: readonly EntityMeta['spaceId'][];
202
+ typeDxns: readonly EntityMeta['typeDXN'][];
172
203
  inverted?: boolean;
173
204
  includeAllQueues?: boolean;
174
205
  queueIds?: readonly string[] | null;
175
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
206
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
176
207
  Effect.gen(function* () {
177
208
  if (spaceIds.length === 0 && (!queueIds || queueIds.length === 0)) {
178
209
  return [];
@@ -185,7 +216,7 @@ export class ObjectMetaIndex implements Index {
185
216
 
186
217
  const sql = yield* SqlClient.SqlClient;
187
218
  const sourceCondition = buildSourceCondition(sql, spaceIds, includeAllQueues, queueIds);
188
- const rows = yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
219
+ const rows = yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
189
220
  return rows.map((row) => ({
190
221
  ...row,
191
222
  deleted: !!row.deleted,
@@ -194,16 +225,19 @@ export class ObjectMetaIndex implements Index {
194
225
  const sql = yield* SqlClient.SqlClient;
195
226
  const sourceCondition = buildSourceCondition(sql, spaceIds, includeAllQueues, queueIds);
196
227
  const typeWhere = sql.or(
197
- typeDxns.map((typeDxn) => {
198
- const parsedType = DXN.tryParse(typeDxn)?.asTypeDXN();
199
- return parsedType && parsedType.version === undefined
200
- ? sql.or([sql`typeDxn = ${typeDxn}`, sql`typeDxn LIKE ${_escapeLikePrefix(typeDxn)} ESCAPE '\\'`])
201
- : sql`typeDxn = ${typeDxn}`;
228
+ typeDxns.map((typeDXN) => {
229
+ const normalized = _normalizeTypeUri(typeDXN);
230
+ const parsedDxn = DXN.isDXN(normalized) ? normalized : undefined;
231
+ const hasNoVersion = parsedDxn !== undefined && DXN.getVersion(parsedDxn) === undefined;
232
+ const exactMatch = sql`typeDXN = ${normalized}`;
233
+ return hasNoVersion
234
+ ? sql.or([exactMatch, sql`typeDXN LIKE ${_escapeLikePrefix(normalized)} ESCAPE '\\'`])
235
+ : exactMatch;
202
236
  }),
203
237
  );
204
238
  const rows = inverted
205
- ? yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND NOT ${typeWhere}`
206
- : yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND ${typeWhere}`;
239
+ ? yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND NOT ${typeWhere}`
240
+ : yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND ${typeWhere}`;
207
241
  return rows.map((row) => ({
208
242
  ...row,
209
243
  deleted: !!row.deleted,
@@ -211,21 +245,21 @@ export class ObjectMetaIndex implements Index {
211
245
  }),
212
246
  );
213
247
 
214
- queryRelations = Effect.fn('ObjectMetaIndex.queryRelations')(
248
+ queryRelations = Effect.fn('EntityMetaIndex.queryRelations')(
215
249
  ({
216
250
  endpoint,
217
251
  anchorDxns,
218
252
  }: {
219
253
  endpoint: 'source' | 'target';
220
254
  anchorDxns: readonly string[];
221
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
255
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
222
256
  Effect.gen(function* () {
223
257
  if (anchorDxns.length === 0) {
224
258
  return [];
225
259
  }
226
260
  const sql = yield* SqlClient.SqlClient;
227
261
  const column = endpoint === 'source' ? 'source' : 'target';
228
- const rows = yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE entityKind = 'relation' AND ${sql.in(
262
+ const rows = yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE entityKind = 'relation' AND ${sql.in(
229
263
  column,
230
264
  anchorDxns,
231
265
  )}`;
@@ -237,7 +271,7 @@ export class ObjectMetaIndex implements Index {
237
271
  );
238
272
 
239
273
  // TODO(dmaretskyi): Update recordId on objects so that we don't need to look it up separately.
240
- update = Effect.fn('ObjectMetaIndex.update')(
274
+ update = Effect.fn('EntityMetaIndex.update')(
241
275
  (objects: IndexerObject[]): Effect.Effect<void, SqlError.SqlError, SqlClient.SqlClient> =>
242
276
  Effect.gen(function* () {
243
277
  const sql = yield* SqlClient.SqlClient;
@@ -246,7 +280,7 @@ export class ObjectMetaIndex implements Index {
246
280
  objects,
247
281
  (object) =>
248
282
  Effect.gen(function* () {
249
- const { spaceId, queueId, documentId, data } = object;
283
+ const { spaceId, queueId, queueNamespace, documentId, data } = object;
250
284
 
251
285
  // Extract metadata (Logic emulating Echo APIs as strict imports are unavailable).
252
286
  const castData = data;
@@ -274,7 +308,9 @@ export class ObjectMetaIndex implements Index {
274
308
 
275
309
  // Extract metadata.
276
310
  const entityKind = castData[ATTR_RELATION_SOURCE] ? 'relation' : 'object';
277
- const typeDxn = castData[ATTR_TYPE] ? String(castData[ATTR_TYPE]) : 'type';
311
+ // Type identifier as stored on `system.type`: a typename DXN for static schemas,
312
+ // an `echo:` EID for stored (dynamic) schemas.
313
+ const typeDXN = URI.make(castData[ATTR_TYPE] ? String(castData[ATTR_TYPE]) : 'type');
278
314
  const deleted = castData[ATTR_DELETED] ? 1 : 0;
279
315
  // Relations.
280
316
  const source = entityKind === 'relation' ? (castData[ATTR_RELATION_SOURCE] ?? null) : null;
@@ -282,32 +318,36 @@ export class ObjectMetaIndex implements Index {
282
318
  // Parent (nullable).
283
319
  const parent = castData[ATTR_PARENT] ?? null;
284
320
 
285
- const sourceTimestamp = object.updatedAt;
321
+ const updatedAtTimestamp = object.updatedAt;
322
+ // Prefer the creation timestamp stored in the document (survives compaction/migrations).
323
+ // Fall back to the automerge-derived updatedAt for legacy objects that predate this field.
324
+ const createdAtTimestamp = object.createdAt ?? updatedAtTimestamp;
286
325
 
287
326
  if (existing.length > 0) {
288
327
  yield* sql`
289
328
  UPDATE objectMeta SET
290
329
  version = ${version},
330
+ queueNamespace = ${queueNamespace ?? ''},
291
331
  entityKind = ${entityKind},
292
- typeDxn = ${typeDxn},
332
+ typeDXN = ${typeDXN},
293
333
  deleted = ${deleted},
294
334
  source = ${source},
295
335
  target = ${target},
296
336
  parent = ${parent},
297
- updatedAt = ${sourceTimestamp}
337
+ updatedAt = ${updatedAtTimestamp}
298
338
  WHERE recordId = ${existing[0].recordId}
299
339
  `;
300
340
  } else {
301
341
  yield* sql`
302
342
  INSERT INTO objectMeta (
303
- objectId, queueId, spaceId, documentId,
304
- entityKind, typeDxn, deleted, source, target, parent, version,
343
+ objectId, queueId, queueNamespace, spaceId, documentId,
344
+ entityKind, typeDXN, deleted, source, target, parent, version,
305
345
  createdAt, updatedAt
306
346
  ) VALUES (
307
- ${objectId}, ${queueId ?? ''}, ${spaceId}, ${documentId ?? ''},
308
- ${entityKind}, ${typeDxn}, ${deleted},
347
+ ${objectId}, ${queueId ?? ''}, ${queueNamespace ?? ''}, ${spaceId}, ${documentId ?? ''},
348
+ ${entityKind}, ${typeDXN}, ${deleted},
309
349
  ${source}, ${target}, ${parent}, ${version},
310
- ${sourceTimestamp}, ${sourceTimestamp}
350
+ ${createdAtTimestamp}, ${updatedAtTimestamp}
311
351
  )
312
352
  `;
313
353
  }
@@ -318,10 +358,10 @@ export class ObjectMetaIndex implements Index {
318
358
  );
319
359
 
320
360
  /**
321
- * Look up `recordIds` for objects that are already stored in the ObjectMetaIndex.
361
+ * Look up `recordIds` for objects that are already stored in the EntityMetaIndex.
322
362
  * Mutates the objects in place.
323
363
  */
324
- lookupRecordIds = Effect.fn('ObjectMetaIndex.lookupRecordIds')(
364
+ lookupRecordIds = Effect.fn('EntityMetaIndex.lookupRecordIds')(
325
365
  (objects: IndexerObject[]): Effect.Effect<void, SqlError.SqlError, SqlClient.SqlClient> =>
326
366
  Effect.gen(function* () {
327
367
  const sql = yield* SqlClient.SqlClient;
@@ -346,7 +386,7 @@ export class ObjectMetaIndex implements Index {
346
386
  if (result.length === 0) {
347
387
  // TODO(mykola): Handle this case gracefully.
348
388
  return yield* Effect.die(
349
- new Error(`Object not found in ObjectMetaIndex: ${spaceId}/${documentId ?? queueId}/${objectId}`),
389
+ new Error(`Object not found in EntityMetaIndex: ${spaceId}/${documentId ?? queueId}/${objectId}`),
350
390
  );
351
391
  }
352
392
  object.recordId = result[0].recordId;
@@ -357,15 +397,15 @@ export class ObjectMetaIndex implements Index {
357
397
  /**
358
398
  * Look up object metadata by recordIds.
359
399
  */
360
- lookupByRecordIds = Effect.fn('ObjectMetaIndex.lookupByRecordIds')(
361
- (recordIds: number[]): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
400
+ lookupByRecordIds = Effect.fn('EntityMetaIndex.lookupByRecordIds')(
401
+ (recordIds: number[]): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
362
402
  Effect.gen(function* () {
363
403
  if (recordIds.length === 0) {
364
404
  return [];
365
405
  }
366
406
 
367
407
  const sql = yield* SqlClient.SqlClient;
368
- const rows = yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sql.in('recordId', recordIds)}`;
408
+ const rows = yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sql.in('recordId', recordIds)}`;
369
409
 
370
410
  return rows.map((row) => ({
371
411
  ...row,
@@ -374,19 +414,42 @@ export class ObjectMetaIndex implements Index {
374
414
  }),
375
415
  );
376
416
 
417
+ /**
418
+ * Look up object metadata by object id across one or more spaces (space db and queue items).
419
+ */
420
+ queryObjectIds = Effect.fn('EntityMetaIndex.queryObjectIds')(
421
+ (query: {
422
+ spaceIds: readonly EntityMeta['spaceId'][];
423
+ objectIds: readonly EntityMeta['objectId'][];
424
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
425
+ Effect.gen(function* () {
426
+ if (query.spaceIds.length === 0 || query.objectIds.length === 0) {
427
+ return [];
428
+ }
429
+
430
+ const sql = yield* SqlClient.SqlClient;
431
+ const rows =
432
+ yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sql.in('spaceId', query.spaceIds)} AND ${sql.in('objectId', query.objectIds)}`;
433
+ return rows.map((row) => ({
434
+ ...row,
435
+ deleted: !!row.deleted,
436
+ }));
437
+ }),
438
+ );
439
+
377
440
  /**
378
441
  * Look up object metadata by objectId, spaceId, and queueId.
379
442
  */
380
- lookupByObjectId = Effect.fn('ObjectMetaIndex.lookupByObjectId')(
443
+ lookupByObjectId = Effect.fn('EntityMetaIndex.lookupByObjectId')(
381
444
  (query: {
382
445
  objectId: string;
383
446
  spaceId: string;
384
447
  queueId: string;
385
- }): Effect.Effect<ObjectMeta | null, SqlError.SqlError, SqlClient.SqlClient> =>
448
+ }): Effect.Effect<EntityMeta | null, SqlError.SqlError, SqlClient.SqlClient> =>
386
449
  Effect.gen(function* () {
387
450
  const sql = yield* SqlClient.SqlClient;
388
451
  const rows =
389
- yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND queueId = ${query.queueId} AND objectId = ${query.objectId} LIMIT 1`;
452
+ yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND queueId = ${query.queueId} AND objectId = ${query.objectId} LIMIT 1`;
390
453
 
391
454
  if (rows.length === 0) {
392
455
  return null;
@@ -402,7 +465,7 @@ export class ObjectMetaIndex implements Index {
402
465
  /**
403
466
  * Query objects by timestamp range.
404
467
  */
405
- queryByTimeRange = Effect.fn('ObjectMetaIndex.queryByTimeRange')(
468
+ queryByTimeRange = Effect.fn('EntityMetaIndex.queryByTimeRange')(
406
469
  (query: {
407
470
  spaceIds: readonly string[];
408
471
  updatedAfter?: number;
@@ -411,7 +474,7 @@ export class ObjectMetaIndex implements Index {
411
474
  createdBefore?: number;
412
475
  includeAllQueues?: boolean;
413
476
  queueIds?: readonly string[] | null;
414
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
477
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
415
478
  Effect.gen(function* () {
416
479
  if (query.spaceIds.length === 0 && (!query.queueIds || query.queueIds.length === 0)) {
417
480
  return [];
@@ -441,8 +504,8 @@ export class ObjectMetaIndex implements Index {
441
504
 
442
505
  const rows =
443
506
  timeConditions.length > 0
444
- ? yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND ${sql.and(timeConditions)}`
445
- : yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
507
+ ? yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition} AND ${sql.and(timeConditions)}`
508
+ : yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sourceCondition}`;
446
509
 
447
510
  return rows.map((row) => ({
448
511
  ...row,
@@ -453,20 +516,26 @@ export class ObjectMetaIndex implements Index {
453
516
 
454
517
  /**
455
518
  * Query children by parent object ids.
519
+ * Matches both:
520
+ * - Objects whose `parent` field references one of the given parent ids (standard parent/child hierarchy).
521
+ * - Queue items whose `queueId` equals one of the parent ids (e.g. items inside a Feed, since a feed's queue
522
+ * DXN uses the feed's object id as its queue id — see `Feed.getQueueUri`).
456
523
  */
457
- queryChildren = Effect.fn('ObjectMetaIndex.queryChildren')(
524
+ queryChildren = Effect.fn('EntityMetaIndex.queryChildren')(
458
525
  (query: {
459
526
  spaceId: SpaceId[];
460
- parentIds: ObjectId[];
461
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
527
+ parentIds: EntityId[];
528
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> =>
462
529
  Effect.gen(function* () {
463
530
  if (query.parentIds.length === 0) {
464
531
  return [];
465
532
  }
466
533
 
467
534
  const sql = yield* SqlClient.SqlClient;
535
+ const parentDzns = query.parentIds.map((id) => EID.make({ entityId: id }));
536
+ const parentDxns = parentDzns;
468
537
  const rows =
469
- yield* sql<ObjectMeta>`SELECT * FROM objectMeta WHERE spaceId IN ${sql.in(query.spaceId)} AND parent IN ${sql.in(query.parentIds.map((id) => DXN.fromLocalObjectId(id).toString()))}`;
538
+ yield* sql<EntityMeta>`SELECT * FROM objectMeta WHERE ${sql.in('spaceId', query.spaceId)} AND (${sql.in('parent', parentDxns)} OR ${sql.in('queueId', query.parentIds)})`;
470
539
 
471
540
  return rows.map((row) => ({
472
541
  ...row,