@procedures/mongo 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -253,675 +253,35 @@ Check if a default connection exists.
253
253
 
254
254
  ### Registered Procedures
255
255
 
256
- All procedures are automatically registered when the package is imported. They can be called via `client.call()` or used directly.
257
-
258
- #### Database Procedures
259
-
260
- ##### `mongo.database.ping`
261
-
262
- Test database connectivity and measure latency.
263
-
264
- ```typescript
265
- // Via RPC
266
- const result = await client.exec(
267
- ["mongo", "database", "ping"],
268
- { timeout: 5000 }
269
- );
270
-
271
- // Direct import
272
- import { pingProcedure } from "@procedures/mongo";
273
- ```
274
-
275
- **Input:**
276
- ```typescript
277
- interface PingInput {
278
- timeout?: number; // Optional timeout in milliseconds
279
- }
280
- ```
281
-
282
- **Output:**
283
- ```typescript
284
- interface PingOutput {
285
- latencyMs: number; // Response time in milliseconds
286
- ok: boolean; // Server status
287
- }
288
- ```
289
-
290
- ##### `mongo.database.info`
291
-
292
- Get database information and statistics.
293
-
294
- ```typescript
295
- const result = await client.exec(
296
- ["mongo", "database", "info"],
297
- { includeStats: true }
298
- );
299
- ```
300
-
301
- **Input:**
302
- ```typescript
303
- interface InfoInput {
304
- includeStats?: boolean;
305
- }
306
- ```
307
-
308
- **Output:**
309
- ```typescript
310
- interface DatabaseInfo {
311
- name: string; // Database name
312
- collections: number; // Number of collections
313
- views: number; // Number of views
314
- sizeOnDisk: number; // Size on disk in bytes
315
- empty: boolean; // Whether database is empty
316
- }
317
- ```
318
-
319
- #### Collection Procedures
320
-
321
- ##### `mongo.collections.list`
322
-
323
- List all collections in the database.
324
-
325
- ```typescript
326
- const result = await client.exec(
327
- ["mongo", "collections", "list"],
328
- { filter: "user" }
329
- );
330
- ```
331
-
332
- **Input:**
333
- ```typescript
334
- interface ListCollectionsInput {
335
- filter?: string; // Filter by name pattern
336
- }
337
- ```
338
-
339
- **Output:**
340
- ```typescript
341
- interface ListCollectionsOutput {
342
- collections: string[]; // Collection names
343
- }
344
- ```
345
-
346
- ##### `mongo.collections.create`
347
-
348
- Create a new collection.
349
-
350
- ```typescript
351
- const result = await client.exec(
352
- ["mongo", "collections", "create"],
353
- {
354
- name: "logs",
355
- options: {
356
- capped: true,
357
- size: 10485760, // 10MB
358
- max: 1000
359
- }
360
- }
361
- );
362
- ```
363
-
364
- **Input:**
365
- ```typescript
366
- interface CreateCollectionInput {
367
- name: string;
368
- options?: CollectionOptions;
369
- }
370
-
371
- interface CollectionOptions {
372
- capped?: boolean;
373
- size?: number; // Max size in bytes (for capped)
374
- max?: number; // Max documents (for capped)
375
- validator?: Document; // JSON Schema validator
376
- }
377
- ```
378
-
379
- **Output:**
380
- ```typescript
381
- interface CreateCollectionOutput {
382
- created: boolean;
383
- name: string;
384
- }
385
- ```
386
-
387
- ##### `mongo.collections.drop`
388
-
389
- Drop a collection.
390
-
391
- ```typescript
392
- const result = await client.exec(
393
- ["mongo", "collections", "drop"],
394
- { name: "old_collection" }
395
- );
396
- ```
397
-
398
- **Input:**
399
- ```typescript
400
- interface DropCollectionInput {
401
- name: string;
402
- }
403
- ```
404
-
405
- **Output:**
406
- ```typescript
407
- interface DropCollectionOutput {
408
- dropped: boolean;
409
- }
410
- ```
411
-
412
- ##### `mongo.collections.stats`
413
-
414
- Get collection statistics.
415
-
416
- ```typescript
417
- const result = await client.exec(
418
- ["mongo", "collections", "stats"],
419
- {},
420
- { metadata: { collection: "users" } }
421
- );
422
- ```
423
-
424
- **Input:**
425
- ```typescript
426
- interface StatsInput {
427
- // Collection from metadata
428
- }
429
- ```
430
-
431
- **Output:**
432
- ```typescript
433
- interface CollectionStats {
434
- count: number; // Number of documents
435
- size: number; // Size of documents in bytes
436
- avgObjSize: number; // Average document size
437
- storageSize: number; // Total storage size
438
- nindexes: number; // Number of indexes
439
- totalIndexSize: number; // Total index size
440
- }
441
- ```
442
-
443
- #### Document Procedures
444
-
445
- ##### `mongo.documents.find`
446
-
447
- Find documents with pagination, filtering, sorting, and projection.
448
-
449
- ```typescript
450
- const result = await client.exec(
451
- ["mongo", "documents", "find"],
452
- {
453
- query: { status: "active", age: { $gte: 18 } },
454
- projection: { name: 1, email: 1 },
455
- sort: { createdAt: -1 },
456
- page: 1,
457
- limit: 20
458
- },
459
- { metadata: { collection: "users" } }
460
- );
461
- ```
462
-
463
- **Input:**
464
- ```typescript
465
- interface FindInput {
466
- query?: DocumentQuery; // MongoDB filter
467
- projection?: Record<string, 0 | 1>; // Field projection
468
- sort?: SortSpec; // Sort specification
469
- page?: number; // Page number (1-indexed)
470
- limit?: number; // Items per page (max 100)
471
- }
472
-
473
- type DocumentQuery = Filter<Document>;
474
- type SortSpec = Record<string, 1 | -1>;
475
- ```
476
-
477
- **Output:**
478
- ```typescript
479
- interface FindOutput {
480
- documents: MongoDocument[];
481
- pagination: PaginationOutput;
482
- }
483
-
484
- interface PaginationOutput {
485
- page: number;
486
- limit: number;
487
- total: number;
488
- totalPages: number;
489
- hasNext: boolean;
490
- hasPrev: boolean;
491
- }
492
- ```
493
-
494
- ##### `mongo.documents.get`
495
-
496
- Get a single document by ID.
497
-
498
- ```typescript
499
- const result = await client.exec(
500
- ["mongo", "documents", "get"],
501
- { id: "507f1f77bcf86cd799439011" },
502
- { metadata: { collection: "users" } }
503
- );
504
- ```
505
-
506
- **Input:**
507
- ```typescript
508
- interface GetInput {
509
- id: string; // Document ID (converted to ObjectId)
510
- }
511
- ```
512
-
513
- **Output:**
514
- ```typescript
515
- interface GetOutput {
516
- document: MongoDocument | null;
517
- }
518
- ```
519
-
520
- ##### `mongo.documents.insert`
521
-
522
- Insert one or more documents.
523
-
524
- ```typescript
525
- // Insert single document
526
- const result = await client.exec(
527
- ["mongo", "documents", "insert"],
528
- {
529
- documents: {
530
- name: "John Doe",
531
- email: "john@example.com",
532
- createdAt: new Date()
533
- }
534
- },
535
- { metadata: { collection: "users" } }
536
- );
537
-
538
- // Insert multiple documents
539
- const result = await client.exec(
540
- ["mongo", "documents", "insert"],
541
- {
542
- documents: [
543
- { name: "Alice" },
544
- { name: "Bob" }
545
- ]
546
- },
547
- { metadata: { collection: "users" } }
548
- );
549
- ```
550
-
551
- **Input:**
552
- ```typescript
553
- interface InsertInput {
554
- documents: MongoDocument | MongoDocument[];
555
- }
556
- ```
557
-
558
- **Output:**
559
- ```typescript
560
- interface InsertOutput {
561
- insertedCount: number;
562
- insertedIds: string[];
563
- }
564
- ```
565
-
566
- ##### `mongo.documents.update`
567
-
568
- Update one document by `id`, or the documents that match a `filter`.
569
-
570
- A write needs exactly one of `id` or `filter`. An unknown field is an error: `query` is the field of `find` and `count`, not of `update`. An empty filter matches every document, so it needs `confirm: true`.
571
-
572
- ```typescript
573
- // Update one document by ID
574
- const result = await client.exec(
575
- ["mongo", "documents", "update"],
576
- {
577
- collection: "users",
578
- id: "...",
579
- update: { $set: { status: "inactive" } },
580
- upsert: false
581
- }
582
- );
583
-
584
- // Update multiple documents
585
- const result = await client.exec(
586
- ["mongo", "documents", "update"],
587
- {
588
- collection: "users",
589
- filter: { status: "pending" },
590
- update: { $set: { status: "active" } },
591
- multi: true
592
- }
593
- );
594
- ```
595
-
596
- **Input:**
597
- ```typescript
598
- interface UpdateInput {
599
- collection?: string; // Or metadata.collection
600
- database?: string; // Or metadata.database
601
- id?: string; // Exactly one of id and filter
602
- idType?: "auto" | "objectId" | "string";
603
- filter?: DocumentQuery;
604
- update: DocumentUpdate;
605
- multi?: boolean; // Update all matching documents
606
- upsert?: boolean; // Insert if not found
607
- confirm?: boolean; // Required for an empty filter
608
- }
609
-
610
- type DocumentUpdate = UpdateFilter<Document>;
611
- ```
612
-
613
- An upsert by `id` in the "auto" mode updates the document with that ID in either form. When no document matches, the new document gets the ObjectId form of the ID.
614
-
615
- **Output:**
616
- ```typescript
617
- interface UpdateOutput {
618
- acknowledged: boolean;
619
- matchedCount: number;
620
- modifiedCount: number;
621
- upsertedId: string | null;
622
- upsertedCount: number;
623
- }
624
- ```
625
-
626
- ##### `mongo.documents.delete`
627
-
628
- Delete one document by `id`, or the documents that match a `filter`. The rules of `update` apply: exactly one target, no unknown fields, and `confirm: true` for an empty filter.
629
-
630
- ```typescript
631
- // Delete one document by ID
632
- const result = await client.exec(
633
- ["mongo", "documents", "delete"],
634
- { collection: "users", id: "..." }
635
- );
636
-
637
- // Delete multiple documents
638
- const result = await client.exec(
639
- ["mongo", "documents", "delete"],
640
- {
641
- collection: "users",
642
- filter: { status: "inactive" },
643
- multi: true
644
- }
645
- );
646
- ```
647
-
648
- **Input:**
649
- ```typescript
650
- interface DeleteInput {
651
- collection?: string; // Or metadata.collection
652
- database?: string; // Or metadata.database
653
- id?: string; // Exactly one of id and filter
654
- idType?: "auto" | "objectId" | "string";
655
- filter?: DocumentQuery;
656
- multi?: boolean; // Delete all matching documents
657
- confirm?: boolean; // Required for an empty filter
658
- }
659
- ```
660
-
661
- **Output:**
662
- ```typescript
663
- interface DeleteOutput {
664
- acknowledged: boolean;
665
- deletedCount: number;
666
- }
667
- ```
668
-
669
- ##### `mongo.documents.count`
670
-
671
- Count documents matching a query.
672
-
673
- ```typescript
674
- const result = await client.exec(
675
- ["mongo", "documents", "count"],
676
- { query: { status: "active" } },
677
- { metadata: { collection: "users" } }
678
- );
679
- ```
680
-
681
- **Input:**
682
- ```typescript
683
- interface CountInput {
684
- query?: DocumentQuery;
685
- }
686
- ```
687
-
688
- **Output:**
689
- ```typescript
690
- interface CountOutput {
691
- count: number;
692
- }
693
- ```
694
-
695
- ##### `mongo.documents.aggregate`
696
-
697
- Execute an aggregation pipeline.
698
-
699
- ```typescript
700
- const result = await client.exec(
701
- ["mongo", "documents", "aggregate"],
702
- {
703
- pipeline: [
704
- { $match: { status: "active" } },
705
- { $group: { _id: "$country", count: { $sum: 1 } } },
706
- { $sort: { count: -1 } }
707
- ],
708
- options: {
709
- allowDiskUse: true,
710
- maxTimeMS: 5000
711
- }
712
- },
713
- { metadata: { collection: "users" } }
714
- );
715
- ```
716
-
717
- **Input:**
718
- ```typescript
719
- interface AggregateInput {
720
- pipeline: AggregationStage[];
721
- options?: AggregationOptions;
722
- }
723
-
724
- type AggregationStage = Document;
725
-
726
- interface AggregationOptions {
727
- allowDiskUse?: boolean;
728
- maxTimeMS?: number;
729
- }
730
- ```
731
-
732
- **Output:**
733
- ```typescript
734
- interface AggregateOutput {
735
- results: MongoDocument[];
736
- }
737
- ```
738
-
739
- ##### `mongo.documents.distinct`
740
-
741
- The procedure gives the distinct values of one field in the documents that match `query`. The default `query` matches all documents. An array field gives each of its elements. Use dots for an embedded field, for example `address.city`.
742
-
743
- ```typescript
744
- const { values } = await client.exec(
745
- ["mongo", "documents", "distinct"],
746
- { collection: "users", field: "country", query: { status: "active" } }
747
- );
748
- ```
749
-
750
- ##### `mongo.documents.bulk`
751
-
752
- The procedure sends several writes in one request. The operations have the form of the `bulkWrite` of the driver: `insertOne`, `updateOne`, `updateMany`, `deleteOne` and `deleteMany`. By default, the operations run in order, and the first failure stops the rest. With `ordered: false`, the server tries each operation, and the error tells which ones failed.
753
-
754
- The rules of `update` and `delete` apply to each operation. An empty filter of an update or a delete needs `confirm: true` on the call. An unknown operation or field is an error.
755
-
756
- ```typescript
757
- const result = await client.exec(["mongo", "documents", "bulk"], {
758
- collection: "users",
759
- operations: [
760
- { insertOne: { document: { name: "Di" } } },
761
- { updateOne: { filter: { name: "Ann" }, update: { $set: { status: "active" } } } },
762
- { updateMany: { filter: { status: "new" }, update: { $set: { status: "active" } } } },
763
- { deleteOne: { filter: { name: "Bob" } } },
764
- { deleteMany: { filter: { status: "deleted" } } },
765
- ],
766
- });
767
- // result: { ok, insertedCount, matchedCount, modifiedCount, deletedCount, upsertedCount, insertedIds, upsertedIds }
768
- ```
769
-
770
- The ids in `insertedIds` and `upsertedIds` are text, and their keys are the indexes of the operations.
771
-
772
- ##### `mongo.documents.explain`
773
-
774
- The procedure gives the plan of a find or of an aggregation pipeline. With `pipeline`, it explains an aggregation. Without `pipeline`, it explains a find with `query`, `projection`, `sort` and `limit`. The verbosity `queryPlanner` (the default) gives the plan only. With the verbosities `executionStats` and `allPlansExecution`, the server also does the plan and gives its statistics. As `documents.aggregate` does, the procedure refuses `$out` and `$merge`.
775
-
776
- ```typescript
777
- const { operation, plan } = await client.exec(["mongo", "documents", "explain"], {
778
- collection: "users",
779
- query: { email: "ann@example.com" },
780
- verbosity: "executionStats",
781
- });
782
- console.log(plan.queryPlanner.winningPlan);
783
- ```
784
-
785
- #### Index Procedures
786
-
787
- ##### `mongo.indexes.list`
788
-
789
- List all indexes on a collection.
790
-
791
- ```typescript
792
- const result = await client.exec(
793
- ["mongo", "indexes", "list"],
794
- {},
795
- { metadata: { collection: "users" } }
796
- );
797
- ```
798
-
799
- **Input:**
800
- ```typescript
801
- interface ListIndexesInput {
802
- // Collection from metadata
803
- }
804
- ```
805
-
806
- **Output:**
807
- ```typescript
808
- interface ListIndexesOutput {
809
- indexes: IndexInfo[];
810
- }
811
-
812
- interface IndexInfo {
813
- name: string;
814
- key: IndexSpec;
815
- unique?: boolean;
816
- sparse?: boolean;
817
- }
818
-
819
- type IndexSpec = Record<string, 1 | -1 | "text" | "2dsphere">;
820
- ```
821
-
822
- ##### `mongo.indexes.create`
823
-
824
- Create an index on a collection.
825
-
826
- ```typescript
827
- // Simple index
828
- const result = await client.exec(
829
- ["mongo", "indexes", "create"],
830
- {
831
- keys: { email: 1 },
832
- options: { unique: true }
833
- },
834
- { metadata: { collection: "users" } }
835
- );
836
-
837
- // Compound index
838
- const result = await client.exec(
839
- ["mongo", "indexes", "create"],
840
- {
841
- keys: { status: 1, createdAt: -1 },
842
- options: { name: "status_created_idx" }
843
- },
844
- { metadata: { collection: "users" } }
845
- );
846
-
847
- // Text index
848
- const result = await client.exec(
849
- ["mongo", "indexes", "create"],
850
- {
851
- keys: { title: "text", description: "text" }
852
- },
853
- { metadata: { collection: "articles" } }
854
- );
855
- ```
856
-
857
- **Input:**
858
- ```typescript
859
- interface CreateIndexInput {
860
- keys: IndexSpec;
861
- options?: IndexOptions;
862
- }
863
-
864
- interface IndexOptions {
865
- name?: string;
866
- unique?: boolean;
867
- sparse?: boolean;
868
- background?: boolean;
869
- expireAfterSeconds?: number; // TTL index
870
- }
871
- ```
872
-
873
- **Output:**
874
- ```typescript
875
- interface CreateIndexOutput {
876
- name: string; // Index name
877
- }
878
- ```
879
-
880
- ##### `mongo.indexes.drop`
881
-
882
- Drop an index from a collection.
883
-
884
- ```typescript
885
- const result = await client.exec(
886
- ["mongo", "indexes", "drop"],
887
- { name: "email_1" },
888
- { metadata: { collection: "users" } }
889
- );
890
- ```
891
-
892
- **Input:**
893
- ```typescript
894
- interface DropIndexInput {
895
- name: string; // Index name to drop
896
- }
897
- ```
898
-
899
- **Output:**
900
- ```typescript
901
- interface DropIndexOutput {
902
- dropped: boolean;
903
- }
904
- ```
905
-
906
- #### Change Streams
907
-
908
- ##### `mongo.watch`
909
-
910
- The procedure gives the changes of a collection as a stream. Without `collection`, it gives the changes of each collection of the database. The stream has no end (`metadata.stream: { unbounded: true }`). It stops when the reader stops or the call aborts, and the procedure then closes the change stream.
911
-
912
- A change stream needs a replica set or a sharded cluster. A standalone server refuses it, and the reader gets that error.
913
-
914
- ```typescript
915
- for await (const change of client.execStream(["mongo", "watch"], {
916
- collection: "orders",
917
- pipeline: [{ $match: { operationType: { $in: ["insert", "update"] } } }],
918
- fullDocument: "updateLookup",
919
- })) {
920
- console.log(change.operationType, change.documentKey, change.resumeToken);
921
- }
922
- ```
923
-
924
- Each item has the `operationType`, the namespace `ns`, the `documentKey` and the `resumeToken`. An insert gives the `fullDocument`, and an update gives the `updateDescription`. With `fullDocument: "updateLookup"`, an update also gives the current document. To continue after a stop, give the last `resumeToken` as `resumeAfter`. The `pipeline` accepts only the stages of a change stream: `$match`, `$project`, `$addFields`, `$set`, `$unset`, `$replaceRoot`, `$replaceWith` and `$redact`.
256
+ `import "@procedures/mongo/register"` registers the procedures in the global registry. The package root registers nothing: a host gives `mongoModule` to `createRegistry`. Each procedure of a collection takes `collection`, and each procedure takes an optional `database`. The catalog of the site shows the input and the output schema of each procedure.
257
+
258
+ | Procedure | What it does | Main input |
259
+ |---|---|---|
260
+ | `mongo.database.ping` | Tests the connection and gives the latency. | `timeout` |
261
+ | `mongo.database.info` | Gives the name, the counts and the size of the database. | `includeStats` |
262
+ | `mongo.collections.list` | Lists the collections of the database. | `filter` |
263
+ | `mongo.collections.create` | Makes a collection, capped or with a validator. | `name`, `options` |
264
+ | `mongo.collections.drop` | Removes a collection. | `name` |
265
+ | `mongo.collections.stats` | Gives the count, the sizes and the indexes of a collection. | |
266
+ | `mongo.documents.find` | Finds documents, with a projection, a sort and pages. | `query`, `projection`, `sort`, `page`, `limit` |
267
+ | `mongo.documents.get` | Gives one document by its id. | `id` |
268
+ | `mongo.documents.insert` | Inserts one document or more. | `documents` |
269
+ | `mongo.documents.update` | Updates the document of `id`, or the documents that match `filter`. | `id` or `filter`, `update`, `multi`, `upsert`, `confirm` |
270
+ | `mongo.documents.delete` | Deletes the document of `id`, or the documents that match `filter`. | `id` or `filter`, `multi`, `confirm` |
271
+ | `mongo.documents.count` | Counts the documents that match a query. | `query` |
272
+ | `mongo.documents.aggregate` | Runs an aggregation pipeline. It refuses `$out` and `$merge`. | `pipeline`, `allowDiskUse`, `maxTimeMS` |
273
+ | `mongo.documents.distinct` | Gives the distinct values of a field. An array field gives each of its elements. | `field` (dots for an embedded field), `query` |
274
+ | `mongo.documents.bulk` | Sends several writes in one request, in the form of the `bulkWrite` of the driver. | `operations`, `ordered`, `confirm` |
275
+ | `mongo.documents.explain` | Gives the plan of a find, or of a pipeline. It refuses `$out` and `$merge`. | `query` or `pipeline`, `verbosity` |
276
+ | `mongo.indexes.list` | Lists the indexes of a collection. | |
277
+ | `mongo.indexes.create` | Makes an index. | `keys`, `options` |
278
+ | `mongo.indexes.drop` | Removes an index. | `name` |
279
+ | `mongo.watch` | Gives the changes of a collection as a stream, or of each collection without `collection`. | `collection`, `pipeline`, `fullDocument`, `resumeAfter` |
280
+
281
+ - `update` and `delete` need exactly one target (`id` or `filter`), and refuse unknown fields. An empty filter needs `confirm: true`.
282
+ - The operations of `bulk` are `insertOne`, `updateOne`, `updateMany`, `deleteOne` and `deleteMany`. By default the server does them in order, and the first failure stops the rest. With `ordered: false`, the server tries each operation, and the error tells which ones failed.
283
+ - The verbosity `queryPlanner` of `explain` (the default) gives the plan only. With `executionStats` and `allPlansExecution`, the server also does the plan and gives its statistics.
284
+ - The stream of `watch` has no end (`metadata.stream: { unbounded: true }`). It stops when the reader stops or the call aborts, and the procedure then closes the change stream.
925
285
 
926
286
  ### Type Definitions
927
287
 
@@ -1283,6 +643,105 @@ await client.exec(
1283
643
  );
1284
644
  ```
1285
645
 
646
+ ## Document storage: `MongoDocumentStorage`
647
+
648
+ `MongoDocumentStorage` is a `CollectionStorage` of `@procedures/collections` over MongoDB (roadmap CLI-24). It has flat documents, a change log, versions, queries in the database and partitions. `MongoStorage` keeps its `{ _id, data }` documents.
649
+
650
+ ```ts
651
+ import { MongoDocumentStorage } from "@procedures/mongo";
652
+
653
+ const notes = new MongoDocumentStorage<Note>({ collection: "notes" });
654
+ await notes.set("n1", { title: "Plan", done: false });
655
+ await notes.query({ done: false }, { sort: { title: 1 }, limit: 20 });
656
+
657
+ // One storage for each tenant, on one collection (roadmap CLI-17)
658
+ server.usePartitions("notes", { storageOf: (partition) => notes.forPartition(partition), resolve });
659
+ ```
660
+
661
+ | Option | Use |
662
+ | --- | --- |
663
+ | `collection` | The MongoDB collection |
664
+ | `db` | The database. The default is the database of the default connection. |
665
+ | `partition` | The partition of the storage. `forPartition(partition)` gives the storage of a partition, the same one each time. |
666
+ | `maxTombstones` | The most deletes that the change log keeps in each partition (default 10000) |
667
+ | `queryPolicy` | The query policy of `query()` without a policy of the call |
668
+ | `counters` | The collection of the sequence counters (default `__storage_counters`) |
669
+ | `writeLease` | The time after which a pending number of a write that stopped no longer holds the readers (default `seconds(60)`). A plain number is a `TypeError`. |
670
+ | `client` | The client of the database, for the transactions. The default is the client of the default connection when `db` is absent. |
671
+ | `transactions` | `auto` (the default): a transaction for each write on a replica set or a sharded cluster. `never`: the watermark only. |
672
+
673
+ - **The documents.** A plain object with safe keys is stored flat: its fields beside the fields of the storage (`_key`, `_part`, `_seq`, `_ord`, `_tomb`). Another item is stored as JSON text in `_json`. Both ways give the item back as it was. An item whose `_id` is its id keeps it.
674
+ - **The change log.** Each write gets the next number of the sequence of the collection, and a delete leaves a tombstone. `getChangesSince()` thus gives the exact changes and deletes. A version of another epoch, or a version before the oldest dropped tombstone, gives a full result.
675
+ - **The versions.** `versionOf(id)` gives the number of the last write of an item.
676
+ - **The queries.** `query()` sends the equality of a string, a number or a boolean, and `$in` of them, to MongoDB as a filter. `queryEntries()` of the core then gives the exact result, so a query gives the result of the reference.
677
+ - **Several writers.** On a replica set, a write takes its numbers and writes in one transaction. On a single server, a watermark keeps the log exact. Each write keeps its numbers pending until it ends, and a version stops below the lowest pending number. A write that ends after its lease moves its documents to new numbers.
678
+ - **The case that stays on a single server.** A process that stops between a late write and the move of its documents leaves that change below the watermark. A reader that passed those numbers does not get the change. On a replica set, this case does not occur.
679
+ - **The partitions.** An id cannot have the character U+001F, and each write filter has the partition. A write cannot change a document of another partition.
680
+ - **The tests.** The storage passes the `CollectionStorage` contract on MongoDB 7 in CI. Set `MONGO_TEST_URL` to start them locally.
681
+
682
+ ### The guard of a pipeline: `scopePipeline`
683
+
684
+ `scopePipeline(pipeline, scope)` gives a pipeline that reads only the documents of a filter, on each collection that it reads. `MongoDocumentStorage.aggregate()` uses it with the filter of its partition. An app can use it for its own aggregations over shared collections:
685
+
686
+ ```ts
687
+ import { boundPipeline, scopePipeline } from "@procedures/mongo";
688
+
689
+ const scoped = scopePipeline(pipeline, { match: { tenant: "t1" }, collections: ["orders", "customers"] });
690
+ const { pipeline: bounded, limited } = boundPipeline(scoped, 5000);
691
+ ```
692
+
693
+ - The filter goes before the first stage, into the stages that must be first (`$geoNear`, `$search`, `$vectorSearch`), into a `$match` with `$text`, and into each sub-pipeline (`$lookup`, `$graphLookup`, `$unionWith`, `$facet`).
694
+ - A `$lookup` with `localField` and `foreignField` becomes the form with a pipeline, so the filter applies to the other collection too.
695
+ - The pipeline reads and writes only the collections of the scope (`from`, `coll`, `into`). Give only the collections that the caller can read: a pipeline needs the same right as a direct read.
696
+ - `$out` is refused. `$merge` is refused unless the scope has a `stamp`. Then each written document gets the stamp, and the match key of `$merge` gets its fields.
697
+ - Only the stages of `ALLOWED_PIPELINE_STAGES` pass, so a stage of the server is refused, also a stage that a later MongoDB adds. `$documents` is refused. The JavaScript operators of the server (`$function`, `$accumulator`, `$where`) are refused at each depth.
698
+ - A stage must have exactly one key, and a `$unset` of a field of the scope is refused.
699
+ - The function does not change the pipeline of the caller.
700
+ - `MongoDocumentStorage.aggregate(pipeline, { maxTime, maxResults })` is read-only, reads only its own collection, and keeps the tombstones out. It gives `{ docs, truncated }`. The defaults are `seconds(10)` and 5000 documents. `maxTime` is a duration of `@procedures/client`.
701
+
702
+ ### Materialized views: `ViewMaterializer`
703
+
704
+ A view is a document of a views collection: a pipeline over source collections, whose result goes into a target collection, for one partition. `ViewMaterializer` keeps each target equal to the result of its pipeline, with change streams. Change streams need a replica set.
705
+
706
+ ```ts
707
+ import { ViewMaterializer, viewFilter } from "@procedures/mongo";
708
+
709
+ await db.collection("_aggregations").insertOne({
710
+ _id: "totals",
711
+ _part: "tenant-1",
712
+ collections: ["orders"],
713
+ pipeline: [{ $group: { _id: "$status", count: { $sum: 1 } } }],
714
+ targetCollection: "orderTotals",
715
+ });
716
+ const views = new ViewMaterializer({ db: () => db });
717
+ await views.start();
718
+
719
+ // A reader of the target reads the current generation of the view
720
+ const view = await db.collection("_aggregations").findOne({ _id: "totals" });
721
+ const totals = await db.collection("orderTotals").find(viewFilter(view)).toArray();
722
+ ```
723
+
724
+ - A change of a source starts a pass of its partition after the burst (`debounce`). A pass computes only the views whose sources changed, in the order of their dependencies. A view whose result changed makes its target dirty, so the views that read it are in the same pass.
725
+ - A run reads the result in groups of 1000. Each stored row keeps the hash of its content (`_viewHash`), so a row that did not change writes nothing.
726
+ - The rows of a view are a generation (`_viewGen`), and the status of the view names the current one. A reader of a target filters with `viewFilter(view)`, so it reads one whole generation. A plain query can see two generations.
727
+ - A run writes in one transaction: the changed rows of the current generation, and the removal of the rows that the result does not have. A reader does not see a part of a run.
728
+ - A view with more rows than `maxTransactionRows` (default 10000), or whose transaction fails, writes a whole new generation in groups. Then its status names the new generation. The previous one stays for `generationGrace` (default 60 s, on the time of the server), so a reader of it can finish. Then it goes.
729
+ - A pass in groups holds a lease on its view (`passLease`, default 60 s, on the time of the server), and renews it after each group. Another process leaves the view and the rows of the lease alone. The change of generation needs the lease and the generation of the start of the pass. A pass that loses either one gives way, and the view runs again later.
730
+ - A view in a cycle of views stays idle. Its status is `cycle` until the cycle ends.
731
+ - The status of each view (`ok`, `errored` with `lastError`, `cycle`) goes into the field `_materializerStatus` of its document.
732
+ - Each pipeline goes through `scopePipeline` by default. It reads only the documents of the partition, in its sources, and only the current generation of the target of another view. It cannot have `$merge` or `$out`. The option `scope` gives another guard, with the same filter.
733
+ - A stream that ends opens again after `rewatch`, and a pass catches up. The times of the options are durations (`ms(100)`, `seconds(60)`) of `@procedures/client`.
734
+ - A delete has no document in its change event. It reaches only the views of its partition when its pre-image tells the partition (a collection with `changeStreamPreAndPostImages`). Without a pre-image, a delete, a replace and an update of the partition field reach the views of each partition.
735
+ - In a source of `storageSources`, the id of a `MongoDocumentStorage` document tells the storage partition of such a change. The function of that source gives the partition of the views (`identityPartition` when the two are the same). Else the delete reaches the views of each partition.
736
+ - With pre-images, an update that moves a document to another partition also reaches the views of its old partition.
737
+ - `sourceFields` gives the fields of each source, as paths with dots. An update starts a pass when it changes one of them (at the field, under it or above it), or the partition field. The positions in arrays do not count: `lines.0.price` is `lines.price`. The partition field is a top-level field.
738
+ - The stop has the parts of a transport of a service: `stopIntake()`, `drain(deadline, signal)`, `inFlight()` and `close()`. `stop()` does the four. A host composes them with no adapter.
739
+ - The option `clock` gives the clock of the timers, for example a fake clock in a test. The timers do not keep a process alive.
740
+ - The target is a plain collection that only the materializer writes. Two views of one partition do not share a target. The stored `_id` of a row carries its partition and its generation: by default `{ partition, generation, id }` (`partitionTargetId`), where `id` is the `_id` of the row. Thus two partitions with the same keys share a target.
741
+ - The option `targetId` gives another form of the stored `_id`. Two partitions, or two generations, must not give the same `_id`. The write of a row checks the partition, so the view of the second partition fails, and the row of the first one stays. A form without the generation works only in one transaction.
742
+ - A pass keeps in memory the key of each row of the result (at most `maxRows`) and one group of rows. It keeps nothing between passes.
743
+ - An app with targets from an older materializer (the plain key as `_id`, no generation) needs no migration step. The first pass of each view writes its rows in a generation, and removes the rows of its partition with no generation. A row without the partition field stays.
744
+
1286
745
  ## Integration with Ecosystem
1287
746
 
1288
747
  ### Bundle Packages
@@ -1321,42 +780,12 @@ const call = createTypedCaller<AllProcedures>(client.call);
1321
780
 
1322
781
  ## Package Structure
1323
782
 
1324
- ```
1325
- client-mongo/
1326
- ├── src/
1327
- │ ├── connection.ts # Connection management
1328
- │ ├── types.ts # Core type definitions
1329
- │ ├── procedures.types.ts # Typed procedures interface
1330
- │ ├── register.ts # Auto-registration
1331
- │ ├── index.ts # Main entry point
1332
- │ ├── procedures/
1333
- │ │ ├── schema.ts # Minimal schema helpers
1334
- │ │ ├── index.ts # Procedure barrel export
1335
- │ │ ├── database.ping.ts
1336
- │ │ ├── database.info.ts
1337
- │ │ ├── collections.list.ts
1338
- │ │ ├── collections.create.ts
1339
- │ │ ├── collections.drop.ts
1340
- │ │ ├── collections.stats.ts
1341
- │ │ ├── documents.find.ts
1342
- │ │ ├── documents.get.ts
1343
- │ │ ├── documents.insert.ts
1344
- │ │ ├── documents.update.ts
1345
- │ │ ├── documents.delete.ts
1346
- │ │ ├── documents.count.ts
1347
- │ │ ├── documents.aggregate.ts
1348
- │ │ ├── documents.distinct.ts
1349
- │ │ ├── documents.bulk.ts
1350
- │ │ ├── documents.explain.ts
1351
- │ │ ├── indexes.list.ts
1352
- │ │ ├── indexes.create.ts
1353
- │ │ ├── indexes.drop.ts
1354
- │ │ └── watch.ts # mongo.watch
1355
- ├── dist/ # Compiled output
1356
- ├── package.json
1357
- ├── tsconfig.json
1358
- └── README.md
1359
- ```
783
+ - `src/index.ts`: the entry. It registers nothing.
784
+ - `src/module.ts`: `mongoModule`, the procedures as data. `src/register.ts` registers them in the global registry.
785
+ - `src/connection.ts`: the connection and its management.
786
+ - `src/scope.ts`: the database and the collection of a call.
787
+ - `src/procedures/`: one file for each procedure, with the schemas of the inputs.
788
+ - `src/storage/`: `MongoStorage`, `MongoDocumentStorage`, `scopePipeline` and `ViewMaterializer`.
1360
789
 
1361
790
  ## Development
1362
791