@procedures/mongo 0.8.1 → 0.9.1

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,62 @@ 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
+
1286
702
  ## Integration with Ecosystem
1287
703
 
1288
704
  ### Bundle Packages