@procedures/mongo 0.33.0 → 0.35.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 (104) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +34 -5
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +3 -3
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/procedures/collections.create.d.ts.map +1 -1
  9. package/dist/procedures/collections.create.js +5 -5
  10. package/dist/procedures/collections.create.js.map +1 -1
  11. package/dist/procedures/collections.drop.d.ts.map +1 -1
  12. package/dist/procedures/collections.drop.js +5 -5
  13. package/dist/procedures/collections.drop.js.map +1 -1
  14. package/dist/procedures/collections.list.d.ts.map +1 -1
  15. package/dist/procedures/collections.list.js +4 -4
  16. package/dist/procedures/collections.list.js.map +1 -1
  17. package/dist/procedures/collections.stats.d.ts.map +1 -1
  18. package/dist/procedures/collections.stats.js +4 -4
  19. package/dist/procedures/collections.stats.js.map +1 -1
  20. package/dist/procedures/database.info.d.ts.map +1 -1
  21. package/dist/procedures/database.info.js +4 -4
  22. package/dist/procedures/database.info.js.map +1 -1
  23. package/dist/procedures/database.ping.d.ts.map +1 -1
  24. package/dist/procedures/database.ping.js +4 -4
  25. package/dist/procedures/database.ping.js.map +1 -1
  26. package/dist/procedures/documents.aggregate.d.ts.map +1 -1
  27. package/dist/procedures/documents.aggregate.js +5 -5
  28. package/dist/procedures/documents.aggregate.js.map +1 -1
  29. package/dist/procedures/documents.bulk.d.ts.map +1 -1
  30. package/dist/procedures/documents.bulk.js +3 -3
  31. package/dist/procedures/documents.bulk.js.map +1 -1
  32. package/dist/procedures/documents.count.d.ts.map +1 -1
  33. package/dist/procedures/documents.count.js +5 -5
  34. package/dist/procedures/documents.count.js.map +1 -1
  35. package/dist/procedures/documents.delete.d.ts.map +1 -1
  36. package/dist/procedures/documents.delete.js +3 -3
  37. package/dist/procedures/documents.delete.js.map +1 -1
  38. package/dist/procedures/documents.distinct.d.ts.map +1 -1
  39. package/dist/procedures/documents.distinct.js +5 -5
  40. package/dist/procedures/documents.distinct.js.map +1 -1
  41. package/dist/procedures/documents.explain.d.ts.map +1 -1
  42. package/dist/procedures/documents.explain.js +3 -3
  43. package/dist/procedures/documents.explain.js.map +1 -1
  44. package/dist/procedures/documents.find.d.ts.map +1 -1
  45. package/dist/procedures/documents.find.js +3 -3
  46. package/dist/procedures/documents.find.js.map +1 -1
  47. package/dist/procedures/documents.get.d.ts.map +1 -1
  48. package/dist/procedures/documents.get.js +3 -3
  49. package/dist/procedures/documents.get.js.map +1 -1
  50. package/dist/procedures/documents.insert.d.ts.map +1 -1
  51. package/dist/procedures/documents.insert.js +5 -5
  52. package/dist/procedures/documents.insert.js.map +1 -1
  53. package/dist/procedures/documents.update.d.ts.map +1 -1
  54. package/dist/procedures/documents.update.js +3 -3
  55. package/dist/procedures/documents.update.js.map +1 -1
  56. package/dist/procedures/indexes.create.d.ts.map +1 -1
  57. package/dist/procedures/indexes.create.js +5 -5
  58. package/dist/procedures/indexes.create.js.map +1 -1
  59. package/dist/procedures/indexes.drop.d.ts.map +1 -1
  60. package/dist/procedures/indexes.drop.js +5 -5
  61. package/dist/procedures/indexes.drop.js.map +1 -1
  62. package/dist/procedures/indexes.list.d.ts.map +1 -1
  63. package/dist/procedures/indexes.list.js +4 -4
  64. package/dist/procedures/indexes.list.js.map +1 -1
  65. package/dist/procedures/watch.d.ts.map +1 -1
  66. package/dist/procedures/watch.js +3 -3
  67. package/dist/procedures/watch.js.map +1 -1
  68. package/dist/storage/document-storage.d.ts +177 -8
  69. package/dist/storage/document-storage.d.ts.map +1 -1
  70. package/dist/storage/document-storage.js +383 -72
  71. package/dist/storage/document-storage.js.map +1 -1
  72. package/dist/storage/index.d.ts +3 -3
  73. package/dist/storage/index.d.ts.map +1 -1
  74. package/dist/storage/index.js +2 -1
  75. package/dist/storage/index.js.map +1 -1
  76. package/dist/storage/materializer.d.ts +27 -12
  77. package/dist/storage/materializer.d.ts.map +1 -1
  78. package/dist/storage/materializer.js +37 -15
  79. package/dist/storage/materializer.js.map +1 -1
  80. package/package.json +3 -3
  81. package/src/index.ts +11 -3
  82. package/src/procedures/collections.create.ts +5 -9
  83. package/src/procedures/collections.drop.ts +5 -9
  84. package/src/procedures/collections.list.ts +4 -8
  85. package/src/procedures/collections.stats.ts +4 -8
  86. package/src/procedures/database.info.ts +4 -8
  87. package/src/procedures/database.ping.ts +4 -8
  88. package/src/procedures/documents.aggregate.ts +5 -9
  89. package/src/procedures/documents.bulk.ts +3 -5
  90. package/src/procedures/documents.count.ts +5 -9
  91. package/src/procedures/documents.delete.ts +3 -5
  92. package/src/procedures/documents.distinct.ts +5 -9
  93. package/src/procedures/documents.explain.ts +3 -5
  94. package/src/procedures/documents.find.ts +2 -5
  95. package/src/procedures/documents.get.ts +2 -5
  96. package/src/procedures/documents.insert.ts +5 -9
  97. package/src/procedures/documents.update.ts +3 -5
  98. package/src/procedures/indexes.create.ts +5 -9
  99. package/src/procedures/indexes.drop.ts +5 -9
  100. package/src/procedures/indexes.list.ts +4 -8
  101. package/src/procedures/watch.ts +3 -5
  102. package/src/storage/document-storage.ts +441 -71
  103. package/src/storage/index.ts +11 -3
  104. package/src/storage/materializer.ts +54 -22
@@ -4,8 +4,16 @@
4
4
 
5
5
  export { MongoStorage } from "./mongo-storage.js";
6
6
  export type { MongoStorageOptions } from "./mongo-storage.js";
7
- export { MongoDocumentStorage, selectorPrefilter } from "./document-storage.js";
8
- export type { MongoDocumentStorageOptions } from "./document-storage.js";
7
+ // The stored form of a MongoDocumentStorage is an option (roadmap CLI-76)
8
+ export {
9
+ MongoDocumentStorage,
10
+ selectorPrefilter,
11
+ DOCUMENT_STORAGE_FIELDS,
12
+ TOMBSTONE_FIELD,
13
+ DEFAULT_STORED_FORM,
14
+ decodeStoredDocument,
15
+ } from "./document-storage.js";
16
+ export type { MongoDocumentStorageOptions, SelectorPrefilterOptions, StoredForm, DecodeStoredDocumentOptions, StoredDocumentEntry } from "./document-storage.js";
9
17
  export {
10
18
  BLOCKED_PIPELINE_OPERATORS,
11
19
  ALLOWED_PIPELINE_STAGES,
@@ -19,7 +27,7 @@ export {
19
27
  export type { PipelineLimits, PipelineScope } from "./pipeline-scope.js";
20
28
  // Materialized views driven by change streams (Graph's split plan, P4)
21
29
  export { ViewMaterializer, identityPartition, partitionTargetId, viewFilter, VIEW_GENERATION_FIELD, VIEW_HASH_FIELD } from "./materializer.js";
22
- export type { MaterializedView, MaterializedViewStatus, ViewMaterializerOptions, ViewPartitionOf, ViewPipelineScope, ViewTargetId } from "./materializer.js";
30
+ export type { MaterializedView, MaterializedViewStatus, ViewMaterializerOptions, ViewPartitionOf, ViewPipelineScope, ViewStorageSource, ViewTargetId } from "./materializer.js";
23
31
  // A read-only storage of a view for the collection procedures: one generation for each read (roadmap CLI-38)
24
32
  export { MongoViewStorage } from "./view-storage.js";
25
33
  export type { MongoViewStorageOptions } from "./view-storage.js";
@@ -38,8 +38,9 @@
38
38
  *
39
39
  * A change reaches only the views of its partition when its partition is known. A delete has no
40
40
  * document: its partition comes from the pre-image (a collection with
41
- * `changeStreamPreAndPostImages`), or from the id in a source of `storageSources`, through its
42
- * mapping. Else it reaches the views of each partition of its source. With pre-images, an update
41
+ * `changeStreamPreAndPostImages`), or, in a source of `storageSources`, from `partitionOf` of the
42
+ * stored form and the mapping of the source (roadmap CLI-76). Else it reaches the views of each
43
+ * partition of its source. With pre-images, an update
43
44
  * that moves a document to another partition also reaches the views of its old partition.
44
45
  *
45
46
  * Change streams and transactions need a replica set (MongoDB 6 or later). The target of a view is
@@ -52,7 +53,7 @@
52
53
  import { createHash } from "node:crypto";
53
54
  import { BSON, ObjectId, type ChangeStream, type ClientSession, type Collection, type Db, type Document } from "mongodb";
54
55
  import { optionalMs, realClock, type Cancel, type Clock, type Duration } from "@procedures/client";
55
- import { SEPARATOR } from "./document-storage.js";
56
+ import { DEFAULT_STORED_FORM, TOMBSTONE_FIELD, type StoredForm } from "./document-storage.js";
56
57
  import { scopePipeline } from "./pipeline-scope.js";
57
58
 
58
59
  /** The definition of a view, as a document of the views collection. */
@@ -142,13 +143,15 @@ export interface ViewMaterializerOptions {
142
143
  */
143
144
  passLease?: Duration | undefined;
144
145
  /**
145
- * The sources that are collections of a `MongoDocumentStorage`, each with the function that
146
- * gives the partition of the views from the partition of the storage. The stored id of a document
147
- * starts with its storage partition and U+001F. A delete without a pre-image then reaches only the
148
- * views of its partition. Give `identityPartition` when the two partitions are the same. Without a
149
- * function, or when it gives `undefined`, the delete reaches the views of each partition.
146
+ * The sources that are collections of a `MongoDocumentStorage`. For each one, a function that
147
+ * gives the partition of the views from the partition of the storage (the storage has the default
148
+ * form), or a `ViewStorageSource` with the stored form of the storage (`storage.form`) and that
149
+ * function. A change without a pre-image then reaches only the views of its partition: the
150
+ * `partitionOf` of the form reads the storage partition from the document of the change (for a
151
+ * delete, only `documentKey`). Give `identityPartition` when the two partitions are the same.
152
+ * When the form or the function gives `undefined`, the change reaches the views of each partition.
150
153
  */
151
- storageSources?: Readonly<Record<string, ViewPartitionOf>> | undefined;
154
+ storageSources?: Readonly<Record<string, ViewPartitionOf | ViewStorageSource>> | undefined;
152
155
  }
153
156
 
154
157
  /** The function that gives the stored `_id` of a row of a view. */
@@ -212,6 +215,14 @@ export type ViewPartitionOf = (storagePartition: string) => string | undefined;
212
215
  /** The partition of the views is the partition of the storage. */
213
216
  export const identityPartition: ViewPartitionOf = (storagePartition) => storagePartition;
214
217
 
218
+ /** A source of `storageSources` with its stored form (roadmap CLI-76). */
219
+ export interface ViewStorageSource {
220
+ /** The stored form of the storage, for example `storage.form`. Its `partitionOf` reads the storage partition of a document. */
221
+ form: Pick<StoredForm, "partitionOf">;
222
+ /** The partition of the views from the partition of the storage. */
223
+ viewPartitionOf: ViewPartitionOf;
224
+ }
225
+
215
226
  /** A view that the materializer knows. */
216
227
  interface RunningView {
217
228
  view: MaterializedView;
@@ -278,7 +289,7 @@ export class ViewMaterializer {
278
289
  private readonly maxTransactionRows: number;
279
290
  private readonly graceMs: number;
280
291
  private readonly leaseMs: number;
281
- private readonly storageSources: ReadonlyMap<string, ViewPartitionOf>;
292
+ private readonly storageSources: ReadonlyMap<string, ViewStorageSource>;
282
293
  /** False after `stopIntake()`: a change starts no pass. */
283
294
  private intake = false;
284
295
  /** The work of each partition, one at a time: the passes and the rebalances. */
@@ -300,7 +311,15 @@ export class ViewMaterializer {
300
311
  this.maxTransactionRows = options.maxTransactionRows ?? 10_000;
301
312
  this.graceMs = optionalMs(options.generationGrace, "generationGrace") ?? 60_000;
302
313
  this.leaseMs = Math.round(optionalMs(options.passLease, "passLease") ?? 60_000);
303
- this.storageSources = new Map(Object.entries(options.storageSources ?? {}));
314
+ this.storageSources = new Map(
315
+ Object.entries(options.storageSources ?? {}).map(([source, entry]): [string, ViewStorageSource] => {
316
+ const normal = typeof entry === "function" ? { form: DEFAULT_STORED_FORM, viewPartitionOf: entry } : entry;
317
+ if (typeof normal?.form?.partitionOf !== "function" || typeof normal.viewPartitionOf !== "function") {
318
+ throw new TypeError(`The storage source ${source} needs a function, or a form with partitionOf and a viewPartitionOf`);
319
+ }
320
+ return [source, normal];
321
+ })
322
+ );
304
323
  }
305
324
 
306
325
  private log(level: "info" | "warn" | "error", message: string): void {
@@ -310,11 +329,13 @@ export class ViewMaterializer {
310
329
 
311
330
  /**
312
331
  * The guard of a pipeline of a view. A target of another view counts only in its current
313
- * generation. A source document has no generation.
332
+ * generation. A source document has no generation. A tombstone of a `MongoDocumentStorage` (a
333
+ * deleted item) is not a row of a source (roadmap CLI-79).
314
334
  */
315
335
  private scoped(pipeline: Document[], partition: string, view: MaterializedView, generations: readonly ObjectId[]): Document[] {
316
336
  const match = {
317
337
  [this.partitionField]: partition,
338
+ [TOMBSTONE_FIELD]: { $ne: true },
318
339
  $or: [{ [VIEW_GENERATION_FIELD]: { $exists: false } }, { [VIEW_GENERATION_FIELD]: { $in: [...generations] } }],
319
340
  };
320
341
  if (this.options.scope) return this.options.scope(pipeline, partition, view, match);
@@ -570,21 +591,32 @@ export class ViewMaterializer {
570
591
 
571
592
  /**
572
593
  * True for a change without a pre-image (a delete, a replace, an update), in a source of
573
- * `storageSources`, whose id names a storage partition that maps to another partition of the
574
- * views. A document that is in the partition after the change, an id without the separator, a
575
- * mapping that gives `undefined` and a mapping that fails do not tell another partition: false.
594
+ * `storageSources`, whose document names a storage partition that maps to another partition of
595
+ * the views. The document is `fullDocument`, else `fullDocumentBeforeChange`, else `documentKey`,
596
+ * and `partitionOf` of the form reads its storage partition. A document that is in the partition
597
+ * after the change, a document that does not tell its partition, a key (`documentKey`) that tells
598
+ * another storage partition than the document after the change (a move), a mapping that gives
599
+ * `undefined` and a function that fails do not tell another partition: false.
576
600
  */
577
601
  private isOtherPartition(change: Document, source: string, partition: string): boolean {
578
- const viewPartitionOf = this.storageSources.get(source);
579
- if (!viewPartitionOf || change["fullDocumentBeforeChange"]) return false;
602
+ const storageSource = this.storageSources.get(source);
603
+ if (!storageSource || change["fullDocumentBeforeChange"]) return false;
580
604
  if (!["delete", "replace", "update"].includes(change["operationType"] as string)) return false;
581
- if ((change["fullDocument"] as Document | null | undefined)?.[this.partitionField] === partition) return false;
582
- const id = (change["documentKey"] as Document | undefined)?.["_id"];
583
- const at = typeof id === "string" ? id.indexOf(SEPARATOR) : -1;
584
- if (at <= 0) return false;
605
+ const after = change["fullDocument"] as Document | null | undefined;
606
+ if (after?.[this.partitionField] === partition) return false;
607
+ const key = change["documentKey"] as Document | null | undefined;
608
+ const doc = (after ?? (change["fullDocumentBeforeChange"] as Document | null | undefined) ?? key) as Document | null | undefined;
609
+ if (!doc) return false;
585
610
  let viewPartition: string | undefined;
586
611
  try {
587
- viewPartition = viewPartitionOf((id as string).slice(0, at));
612
+ const storagePartition = storageSource.form.partitionOf(doc);
613
+ if (typeof storagePartition !== "string") return false;
614
+ // A document whose key tells another partition than the document after the change can have moved out of this partition
615
+ if (after && key) {
616
+ const before = storageSource.form.partitionOf(key);
617
+ if (before !== undefined && before !== storagePartition) return false;
618
+ }
619
+ viewPartition = storageSource.viewPartitionOf(storagePartition);
588
620
  } catch {
589
621
  return false;
590
622
  }