@nxgt/mongo 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,185 +1,5 @@
1
- // src/definition/define-collection.ts
2
- function defineCollection(config) {
3
- if (!("_id" in config.schema.shape)) {
4
- throw new TypeError(`defineCollection: "${config.name}"'s schema has no _id. Add ` + "`_id: id()`, which fills a new ObjectId on create, or declare the " + "key your documents use.");
5
- }
6
- return Object.freeze({
7
- ...config,
8
- indexes: Object.freeze([...config.indexes ?? []]),
9
- validation: Object.freeze({
10
- level: config.validation?.level ?? "strict",
11
- action: config.validation?.action ?? "error"
12
- })
13
- });
14
- }
15
- function stampsOf(definition) {
16
- const shape = definition.schema.shape;
17
- const has = (name) => (name in shape);
18
- return {
19
- createdAt: has("createdAt"),
20
- updatedAt: has("updatedAt"),
21
- deletedAt: has("deletedAt"),
22
- version: has("version"),
23
- createdBy: has("createdBy"),
24
- updatedBy: has("updatedBy"),
25
- deletedBy: has("deletedBy")
26
- };
27
- }
28
- // src/definition/fields.ts
29
- import { ObjectId as ObjectId2 } from "mongodb";
30
- import { z as z2 } from "zod";
31
-
32
- // src/definition/object-id.ts
33
- import { ObjectId } from "mongodb";
34
- import { z } from "zod";
35
-
36
- // src/errors/data-error.ts
37
- class DataError extends Error {
38
- constructor(message = "Database error", options = {}) {
39
- super(message, options.cause === undefined ? undefined : { cause: options.cause });
40
- this.name = "DataError";
41
- this.code = "DATABASE";
42
- this.collection = options.collection;
43
- this.id = options.id;
44
- this.serverCode = options.serverCode;
45
- this.serverCodeName = options.serverCodeName;
46
- this.index = options.index;
47
- this.keys = options.keys ?? [];
48
- this.values = options.values;
49
- this.issues = options.issues ?? [];
50
- this.expectedVersion = options.expectedVersion;
51
- this.actualVersion = options.actualVersion;
52
- }
53
- }
54
-
55
- class NotFoundError extends DataError {
56
- constructor(message = "Not found", options = {}) {
57
- super(message, options);
58
- this.name = "NotFoundError";
59
- this.code = "NOT_FOUND";
60
- }
61
- }
62
-
63
- class ConflictError extends DataError {
64
- constructor(message = "Duplicate key", options = {}) {
65
- super(message, { serverCode: 11000, ...options });
66
- this.name = "ConflictError";
67
- this.code = "CONFLICT";
68
- }
69
- }
70
-
71
- class ValidationError extends DataError {
72
- constructor(message = "Document failed validation", options = {}) {
73
- super(message, { serverCode: 121, ...options });
74
- this.name = "ValidationError";
75
- this.code = "VALIDATION";
76
- }
77
- }
78
-
79
- class OptimisticLockError extends DataError {
80
- constructor(message = "Version conflict", options = {}) {
81
- super(message, options);
82
- this.name = "OptimisticLockError";
83
- this.code = "OPTIMISTIC_LOCK";
84
- }
85
- }
86
-
87
- class InvalidIdError extends DataError {
88
- constructor(message = "Invalid id", options = {}) {
89
- super(message, options);
90
- this.name = "InvalidIdError";
91
- this.code = "INVALID_ID";
92
- }
93
- }
94
-
95
- class InvalidCursorError extends DataError {
96
- constructor(message = "Invalid cursor", options = {}) {
97
- super(message, options);
98
- this.name = "InvalidCursorError";
99
- this.code = "INVALID_CURSOR";
100
- }
101
- }
102
-
103
- // src/definition/object-id.ts
104
- var HEX_24 = /^[0-9a-fA-F]{24}$/;
105
- function isObjectId(value) {
106
- return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
107
- }
108
- function isObjectIdString(value) {
109
- return typeof value === "string" && HEX_24.test(value);
110
- }
111
- function isValidObjectId(value) {
112
- return isObjectId(value) || isObjectIdString(value);
113
- }
114
- function tryObjectId(value) {
115
- if (isObjectId(value))
116
- return value;
117
- if (isObjectIdString(value))
118
- return ObjectId.createFromHexString(value);
119
- return;
120
- }
121
- function describe(value) {
122
- if (value === null)
123
- return "null";
124
- if (value === undefined)
125
- return "undefined";
126
- if (typeof value === "string")
127
- return `the string ${JSON.stringify(value)}`;
128
- return `a ${typeof value}`;
129
- }
130
- function toObjectId(value, field = "_id") {
131
- const made = tryObjectId(value);
132
- if (made)
133
- return made;
134
- throw new InvalidIdError(`${field}: expected an ObjectId or its 24-character hex string, got ${describe(value)}`, { id: value, keys: [field] });
135
- }
136
- function toObjectIds(values, field = "_id") {
137
- return [...values].map((value) => toObjectId(value, field));
138
- }
139
- function objectIdParam() {
140
- return z.custom(isValidObjectId, {
141
- error: "must be an ObjectId or its 24-character hex string"
142
- }).transform((value) => toObjectId(value));
143
- }
144
-
145
- // src/definition/fields.ts
146
- function objectId() {
147
- return z2.custom(isObjectId, { error: "must be an ObjectId" }).meta({ bsonType: "objectId" });
148
- }
149
- function id() {
150
- return objectId().default(() => new ObjectId2);
151
- }
152
- function timestamps() {
153
- return {
154
- createdAt: z2.date().default(() => new Date),
155
- updatedAt: z2.date().default(() => new Date)
156
- };
157
- }
158
- function softDelete() {
159
- return { deletedAt: z2.date().nullable().default(null) };
160
- }
161
- function optimisticLock() {
162
- return { version: z2.int().nonnegative().default(0) };
163
- }
164
- function actors(actor = objectId()) {
165
- return {
166
- createdBy: actor.nullable().default(null),
167
- updatedBy: actor.nullable().default(null),
168
- deletedBy: actor.nullable().default(null)
169
- };
170
- }
171
- var STAMP_FIELDS = {
172
- id: "_id",
173
- createdAt: "createdAt",
174
- updatedAt: "updatedAt",
175
- deletedAt: "deletedAt",
176
- version: "version",
177
- createdBy: "createdBy",
178
- updatedBy: "updatedBy",
179
- deletedBy: "deletedBy"
180
- };
181
1
  // src/definition/json-schema.ts
182
- import { z as z3 } from "zod";
2
+ import { z } from "zod";
183
3
  var MONGO_JSON_SCHEMA_KEYWORDS = new Set([
184
4
  "additionalItems",
185
5
  "additionalProperties",
@@ -279,7 +99,7 @@ function inline(value, defs, stack) {
279
99
  return out;
280
100
  }
281
101
  function toMongoJsonSchema(schema) {
282
- const json = z3.toJSONSchema(schema, {
102
+ const json = z.toJSONSchema(schema, {
283
103
  target: "draft-4",
284
104
  io: "output",
285
105
  unrepresentable: "any",
@@ -293,6 +113,74 @@ function toMongoJsonSchema(schema) {
293
113
  const definitions = isRecord(json.definitions) ? json.definitions : isRecord(json.$defs) ? json.$defs : {};
294
114
  return inline(json, definitions, []);
295
115
  }
116
+
117
+ // src/errors/data-error.ts
118
+ class DataError extends Error {
119
+ constructor(message = "Database error", options = {}) {
120
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
121
+ this.name = "DataError";
122
+ this.code = "DATABASE";
123
+ this.collection = options.collection;
124
+ this.id = options.id;
125
+ this.serverCode = options.serverCode;
126
+ this.serverCodeName = options.serverCodeName;
127
+ this.index = options.index;
128
+ this.keys = options.keys ?? [];
129
+ this.values = options.values;
130
+ this.issues = options.issues ?? [];
131
+ this.expectedVersion = options.expectedVersion;
132
+ this.actualVersion = options.actualVersion;
133
+ }
134
+ }
135
+
136
+ class NotFoundError extends DataError {
137
+ constructor(message = "Not found", options = {}) {
138
+ super(message, options);
139
+ this.name = "NotFoundError";
140
+ this.code = "NOT_FOUND";
141
+ }
142
+ }
143
+
144
+ class ConflictError extends DataError {
145
+ constructor(message = "Duplicate key", options = {}) {
146
+ super(message, { serverCode: 11000, ...options });
147
+ this.name = "ConflictError";
148
+ this.code = "CONFLICT";
149
+ }
150
+ }
151
+
152
+ class ValidationError extends DataError {
153
+ constructor(message = "Document failed validation", options = {}) {
154
+ super(message, { serverCode: 121, ...options });
155
+ this.name = "ValidationError";
156
+ this.code = "VALIDATION";
157
+ }
158
+ }
159
+
160
+ class OptimisticLockError extends DataError {
161
+ constructor(message = "Version conflict", options = {}) {
162
+ super(message, options);
163
+ this.name = "OptimisticLockError";
164
+ this.code = "OPTIMISTIC_LOCK";
165
+ }
166
+ }
167
+
168
+ class InvalidIdError extends DataError {
169
+ constructor(message = "Invalid id", options = {}) {
170
+ super(message, options);
171
+ this.name = "InvalidIdError";
172
+ this.code = "INVALID_ID";
173
+ }
174
+ }
175
+
176
+ class InvalidCursorError extends DataError {
177
+ constructor(message = "Invalid cursor", options = {}) {
178
+ super(message, options);
179
+ this.name = "InvalidCursorError";
180
+ this.code = "INVALID_CURSOR";
181
+ }
182
+ }
183
+
296
184
  // src/errors/to-data-error.ts
297
185
  function isRecord2(value) {
298
186
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -397,95 +285,7 @@ function toDataError(error, context = {}) {
397
285
  }
398
286
  return new DataError(message || `MongoDB error ${code}`, common);
399
287
  }
400
- // src/pagination/cursor.ts
401
- import { ObjectId as ObjectId3 } from "mongodb";
402
- function isObjectId2(value) {
403
- return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
404
- }
405
- function replacer(key, value) {
406
- const raw = this[key];
407
- if (raw instanceof Date)
408
- return { $date: raw.toISOString() };
409
- if (typeof raw === "bigint")
410
- return { $bigint: raw.toString() };
411
- if (isObjectId2(raw))
412
- return { $oid: raw.toHexString() };
413
- return value;
414
- }
415
- function reviver(_key, value) {
416
- if (value && typeof value === "object" && !Array.isArray(value)) {
417
- const keys = Object.keys(value);
418
- if (keys.length === 1) {
419
- const tagged = value;
420
- if (typeof tagged.$date === "string")
421
- return new Date(tagged.$date);
422
- if (typeof tagged.$bigint === "string")
423
- return BigInt(tagged.$bigint);
424
- if (typeof tagged.$oid === "string")
425
- return new ObjectId3(tagged.$oid);
426
- }
427
- }
428
- return value;
429
- }
430
- function toBase64Url(text) {
431
- let binary = "";
432
- for (const byte of new TextEncoder().encode(text)) {
433
- binary += String.fromCharCode(byte);
434
- }
435
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
436
- }
437
- function fromBase64Url(text) {
438
- const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
439
- const binary = atob(base64 + "=".repeat((4 - base64.length % 4) % 4));
440
- return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
441
- }
442
- function encodeCursor(payload) {
443
- return toBase64Url(JSON.stringify([payload.key, payload.values], replacer));
444
- }
445
- function decodeCursor(cursor, expectedKey) {
446
- let parsed;
447
- try {
448
- parsed = JSON.parse(fromBase64Url(cursor), reviver);
449
- } catch (cause) {
450
- throw new InvalidCursorError("Invalid cursor: it cannot be decoded", {
451
- cause
452
- });
453
- }
454
- if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || !Array.isArray(parsed[1])) {
455
- throw new InvalidCursorError("Invalid cursor: unexpected shape");
456
- }
457
- const [key, values] = parsed;
458
- if (expectedKey !== undefined && key !== expectedKey) {
459
- throw new InvalidCursorError(`Invalid cursor: it was written for the ordering ${key}, not ${expectedKey}`);
460
- }
461
- return { key, values };
462
- }
463
- // src/pagination/page.ts
464
- var DEFAULT_PAGE_SIZE = 20;
465
- var DEFAULT_MAX_PAGE_SIZE = 100;
466
- function positiveInteger(name, value) {
467
- if (!Number.isInteger(value) || value < 1) {
468
- throw new RangeError(`${name} must be an integer of at least 1, not ${value}`);
469
- }
470
- return value;
471
- }
472
- function pageWindow(options = {}, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
473
- const page = positiveInteger("page", options.page ?? 1);
474
- const pageSize = Math.min(positiveInteger("pageSize", options.pageSize ?? DEFAULT_PAGE_SIZE), maxPageSize);
475
- return { page, pageSize, limit: pageSize, skip: (page - 1) * pageSize };
476
- }
477
- function toPage(items, total, window) {
478
- return {
479
- items,
480
- total,
481
- page: window.page,
482
- pageSize: window.pageSize,
483
- pageCount: Math.ceil(total / window.pageSize)
484
- };
485
- }
486
- function cursorLimit(limit, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
487
- return Math.min(positiveInteger("limit", limit ?? DEFAULT_PAGE_SIZE), maxPageSize);
488
- }
288
+
489
289
  // src/sync/index-diff.ts
490
290
  var COLLATION_DEFAULTS = {
491
291
  caseLevel: false,
@@ -709,7 +509,167 @@ async function syncCollections(db, definitions, options = {}) {
709
509
  return reports;
710
510
  }
711
511
 
712
- // src/repository/create-repository.ts
512
+ // src/definition/define-collection.ts
513
+ function defineCollection(config) {
514
+ if (!("_id" in config.schema.shape)) {
515
+ throw new TypeError(`defineCollection: "${config.name}"'s schema has no _id. Add ` + "`_id: id()`, which fills a new ObjectId on create, or declare the " + "key your documents use.");
516
+ }
517
+ return Object.freeze({
518
+ ...config,
519
+ indexes: Object.freeze([...config.indexes ?? []]),
520
+ validation: Object.freeze({
521
+ level: config.validation?.level ?? "strict",
522
+ action: config.validation?.action ?? "error"
523
+ })
524
+ });
525
+ }
526
+ function stampsOf(definition) {
527
+ const shape = definition.schema.shape;
528
+ const has = (name) => (name in shape);
529
+ return {
530
+ createdAt: has("createdAt"),
531
+ updatedAt: has("updatedAt"),
532
+ deletedAt: has("deletedAt"),
533
+ version: has("version"),
534
+ createdBy: has("createdBy"),
535
+ updatedBy: has("updatedBy"),
536
+ deletedBy: has("deletedBy")
537
+ };
538
+ }
539
+
540
+ // src/pagination/page.ts
541
+ var DEFAULT_PAGE_SIZE = 20;
542
+ var DEFAULT_MAX_PAGE_SIZE = 100;
543
+ function positiveInteger(name, value) {
544
+ if (!Number.isInteger(value) || value < 1) {
545
+ throw new RangeError(`${name} must be an integer of at least 1, not ${value}`);
546
+ }
547
+ return value;
548
+ }
549
+ function pageWindow(options = {}, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
550
+ const page = positiveInteger("page", options.page ?? 1);
551
+ const pageSize = Math.min(positiveInteger("pageSize", options.pageSize ?? DEFAULT_PAGE_SIZE), maxPageSize);
552
+ return { page, pageSize, limit: pageSize, skip: (page - 1) * pageSize };
553
+ }
554
+ function toPage(items, total, window) {
555
+ return {
556
+ items,
557
+ total,
558
+ page: window.page,
559
+ pageSize: window.pageSize,
560
+ pageCount: Math.ceil(total / window.pageSize)
561
+ };
562
+ }
563
+ function cursorLimit(limit, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
564
+ return Math.min(positiveInteger("limit", limit ?? DEFAULT_PAGE_SIZE), maxPageSize);
565
+ }
566
+
567
+ // src/collection/context.ts
568
+ function createContext(db, definition, options) {
569
+ const name = definition.name;
570
+ const shape = definition.schema.shape;
571
+ const stamps = stampsOf(definition);
572
+ const session = options.session;
573
+ if (options.softDelete === true && !stamps.deletedAt) {
574
+ throw new TypeError(`getCollection: softDelete needs a "deletedAt" field, and "${name}" has none`);
575
+ }
576
+ if (options.optimisticLock === true && !stamps.version) {
577
+ throw new TypeError(`getCollection: optimisticLock needs a "version" field, and "${name}" has none`);
578
+ }
579
+ return {
580
+ definition,
581
+ db,
582
+ collection: db.collection(name),
583
+ name,
584
+ shape,
585
+ stamps,
586
+ actor: options.actor,
587
+ session,
588
+ sessionOption: session ? { session } : {},
589
+ maxPageSize: options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE,
590
+ hasOwnId: "id" in shape,
591
+ parses: (options.validate ?? "parse") === "parse",
592
+ softDeletes: options.softDelete ?? stamps.deletedAt,
593
+ touches: options.touchUpdatedAt ?? stamps.updatedAt,
594
+ locks: options.optimisticLock ?? stamps.version
595
+ };
596
+ }
597
+ async function run(ctx, fn) {
598
+ try {
599
+ return await fn();
600
+ } catch (error) {
601
+ throw toDataError(error, { collection: ctx.name });
602
+ }
603
+ }
604
+ function notFound(ctx, id) {
605
+ return new NotFoundError(`No document in "${ctx.name}" with _id ${String(id)}`, { collection: ctx.name, id });
606
+ }
607
+
608
+ // src/pagination/cursor.ts
609
+ import { ObjectId } from "mongodb";
610
+ function isObjectId(value) {
611
+ return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
612
+ }
613
+ function replacer(key, value) {
614
+ const raw = this[key];
615
+ if (raw instanceof Date)
616
+ return { $date: raw.toISOString() };
617
+ if (typeof raw === "bigint")
618
+ return { $bigint: raw.toString() };
619
+ if (isObjectId(raw))
620
+ return { $oid: raw.toHexString() };
621
+ return value;
622
+ }
623
+ function reviver(_key, value) {
624
+ if (value && typeof value === "object" && !Array.isArray(value)) {
625
+ const keys = Object.keys(value);
626
+ if (keys.length === 1) {
627
+ const tagged = value;
628
+ if (typeof tagged.$date === "string")
629
+ return new Date(tagged.$date);
630
+ if (typeof tagged.$bigint === "string")
631
+ return BigInt(tagged.$bigint);
632
+ if (typeof tagged.$oid === "string")
633
+ return new ObjectId(tagged.$oid);
634
+ }
635
+ }
636
+ return value;
637
+ }
638
+ function toBase64Url(text) {
639
+ let binary = "";
640
+ for (const byte of new TextEncoder().encode(text)) {
641
+ binary += String.fromCharCode(byte);
642
+ }
643
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
644
+ }
645
+ function fromBase64Url(text) {
646
+ const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
647
+ const binary = atob(base64 + "=".repeat((4 - base64.length % 4) % 4));
648
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
649
+ }
650
+ function encodeCursor(payload) {
651
+ return toBase64Url(JSON.stringify([payload.key, payload.values], replacer));
652
+ }
653
+ function decodeCursor(cursor, expectedKey) {
654
+ let parsed;
655
+ try {
656
+ parsed = JSON.parse(fromBase64Url(cursor), reviver);
657
+ } catch (cause) {
658
+ throw new InvalidCursorError("Invalid cursor: it cannot be decoded", {
659
+ cause
660
+ });
661
+ }
662
+ if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || !Array.isArray(parsed[1])) {
663
+ throw new InvalidCursorError("Invalid cursor: unexpected shape");
664
+ }
665
+ const [key, values] = parsed;
666
+ if (expectedKey !== undefined && key !== expectedKey) {
667
+ throw new InvalidCursorError(`Invalid cursor: it was written for the ordering ${key}, not ${expectedKey}`);
668
+ }
669
+ return { key, values };
670
+ }
671
+
672
+ // src/collection/filters.ts
713
673
  function isRecord3(value) {
714
674
  return typeof value === "object" && value !== null && !Array.isArray(value);
715
675
  }
@@ -725,343 +685,458 @@ function mergeFilters(a, b) {
725
685
  return left;
726
686
  return { $and: [left, right] };
727
687
  }
728
- function createRepository(db, definition, options = {}) {
729
- return build(db, definition, options);
688
+ function live(ctx, withDeleted) {
689
+ return ctx.softDeletes && !withDeleted ? { deletedAt: null } : undefined;
730
690
  }
731
- function build(db, definition, options) {
732
- const name = definition.name;
733
- const collection = db.collection(name);
734
- const shape = definition.schema.shape;
735
- const hasOwnId = "id" in shape;
736
- const stamps = stampsOf(definition);
737
- const session = options.session;
738
- const actor = options.actor;
739
- const maxPageSize = options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE;
740
- const parses = (options.validate ?? "parse") === "parse";
741
- const softDeletes = options.softDelete ?? stamps.deletedAt;
742
- const touches = options.touchUpdatedAt ?? stamps.updatedAt;
743
- const locks = options.optimisticLock ?? stamps.version;
744
- if (options.softDelete === true && !stamps.deletedAt) {
745
- throw new TypeError(`createRepository: softDelete needs a "deletedAt" field, and "${name}" has none`);
746
- }
747
- if (options.optimisticLock === true && !stamps.version) {
748
- throw new TypeError(`createRepository: optimisticLock needs a "version" field, and "${name}" has none`);
691
+ function scoped(ctx, filter, withDeleted) {
692
+ return mergeFilters(isRecord3(filter) ? filter : undefined, live(ctx, withDeleted));
693
+ }
694
+ function requireFilter(ctx, method, filter) {
695
+ if (!isRecord3(filter) || Object.keys(filter).length === 0) {
696
+ throw new TypeError(`${method} needs a filter. Pass \`{ _id: { $exists: true } }\` to target every document of "${ctx.name}".`);
749
697
  }
750
- const run = async (fn) => {
751
- try {
752
- return await fn();
753
- } catch (error) {
754
- throw toDataError(error, { collection: name });
755
- }
756
- };
757
- const sessionOption = session ? { session } : {};
758
- const live = (withDeleted) => softDeletes && !withDeleted ? { deletedAt: null } : undefined;
759
- const scoped = (filter, withDeleted) => mergeFilters(isRecord3(filter) ? filter : undefined, live(withDeleted));
760
- const withId = (document) => {
761
- if (hasOwnId || !isRecord3(document) || document._id === undefined || Object.hasOwn(document, "id")) {
762
- return document;
763
- }
764
- Object.defineProperty(document, "id", {
765
- get: () => String(document._id),
766
- enumerable: true,
767
- configurable: true
768
- });
698
+ }
699
+
700
+ // src/collection/documents.ts
701
+ function withId(ctx, document) {
702
+ if (ctx.hasOwnId || !isRecord3(document) || document._id === undefined || Object.hasOwn(document, "id")) {
769
703
  return document;
770
- };
771
- const notFound = (id) => new NotFoundError(`No document in "${name}" with _id ${String(id)}`, {
772
- collection: name,
773
- id
704
+ }
705
+ Object.defineProperty(document, "id", {
706
+ get: () => String(document._id),
707
+ enumerable: true,
708
+ configurable: true
774
709
  });
775
- const requireFilter = (method, filter) => {
776
- if (!isRecord3(filter) || Object.keys(filter).length === 0) {
777
- throw new TypeError(`${method} needs a filter. Pass \`{ _id: { $exists: true } }\` to target every document of "${name}".`);
778
- }
779
- };
780
- const toDocument = (values) => {
781
- const stamped = { ...values };
782
- if (!hasOwnId)
783
- delete stamped.id;
784
- if (actor !== undefined) {
785
- if (stamps.createdBy && stamped.createdBy === undefined) {
786
- stamped.createdBy = actor;
787
- }
788
- if (stamps.updatedBy && stamped.updatedBy === undefined) {
789
- stamped.updatedBy = actor;
790
- }
791
- }
792
- return parses ? definition.schema.parse(stamped) : stamped;
793
- };
794
- const toUpdate = (patch) => {
795
- if (!isRecord3(patch)) {
796
- throw new TypeError(`update: expected the document's fields or MongoDB's operators, not ${String(patch)}`);
797
- }
798
- const update = isUpdateFilter(patch) ? { ...patch } : {};
799
- const set = isRecord3(update.$set) ? { ...update.$set } : {};
800
- if (!isUpdateFilter(patch)) {
801
- for (const [field, value] of Object.entries(patch)) {
802
- if (value === undefined)
803
- continue;
804
- const schema = shape[field];
805
- if (!schema) {
806
- throw new TypeError(`update: "${name}" has no field "${field}" in its schema`);
807
- }
808
- set[field] = parses ? schema.parse(value) : value;
809
- }
710
+ return document;
711
+ }
712
+ function toDocument(ctx, values) {
713
+ const stamped = { ...values };
714
+ if (!ctx.hasOwnId)
715
+ delete stamped.id;
716
+ if (ctx.actor !== undefined) {
717
+ if (ctx.stamps.createdBy && stamped.createdBy === undefined) {
718
+ stamped.createdBy = ctx.actor;
810
719
  }
811
- if (touches && set.updatedAt === undefined)
812
- set.updatedAt = new Date;
813
- if (actor !== undefined && stamps.updatedBy && set.updatedBy === undefined) {
814
- set.updatedBy = actor;
720
+ if (ctx.stamps.updatedBy && stamped.updatedBy === undefined) {
721
+ stamped.updatedBy = ctx.actor;
815
722
  }
816
- if (Object.keys(set).length > 0)
817
- update.$set = set;
818
- if (locks) {
819
- const inc = isRecord3(update.$inc) ? { ...update.$inc } : {};
820
- inc.version = inc.version ?? 1;
821
- update.$inc = inc;
723
+ }
724
+ return ctx.parses ? ctx.definition.schema.parse(stamped) : stamped;
725
+ }
726
+ function setFromFields(ctx, patch) {
727
+ const set = {};
728
+ for (const [field, value] of Object.entries(patch)) {
729
+ if (value === undefined)
730
+ continue;
731
+ const schema = ctx.shape[field];
732
+ if (!schema) {
733
+ throw new TypeError(`update: "${ctx.name}" has no field "${field}" in its schema`);
822
734
  }
823
- return update;
735
+ set[field] = ctx.parses ? schema.parse(value) : value;
736
+ }
737
+ return set;
738
+ }
739
+ function toUpdate(ctx, patch) {
740
+ if (!isRecord3(patch)) {
741
+ throw new TypeError(`update: expected the document's fields or MongoDB's operators, not ${String(patch)}`);
742
+ }
743
+ const operators = isUpdateFilter(patch);
744
+ const update = operators ? { ...patch } : {};
745
+ const set = {
746
+ ...isRecord3(update.$set) ? update.$set : {},
747
+ ...operators ? {} : setFromFields(ctx, patch)
824
748
  };
825
- const findOne = async (filter, projection) => run(async () => {
826
- const found = await collection.findOne(filter, {
827
- ...sessionOption,
749
+ if (ctx.touches && set.updatedAt === undefined)
750
+ set.updatedAt = new Date;
751
+ if (ctx.actor !== undefined && ctx.stamps.updatedBy && set.updatedBy === undefined) {
752
+ set.updatedBy = ctx.actor;
753
+ }
754
+ if (Object.keys(set).length > 0)
755
+ update.$set = set;
756
+ if (ctx.locks) {
757
+ const inc = isRecord3(update.$inc) ? { ...update.$inc } : {};
758
+ inc.version = inc.version ?? 1;
759
+ update.$inc = inc;
760
+ }
761
+ return update;
762
+ }
763
+
764
+ // src/collection/reads.ts
765
+ function findOne(ctx, filter, projection) {
766
+ return run(ctx, async () => {
767
+ const found = await ctx.collection.findOne(filter, {
768
+ ...ctx.sessionOption,
828
769
  ...projection ? { projection } : {}
829
770
  });
830
- return found === null ? null : withId(found);
771
+ return found === null ? null : withId(ctx, found);
831
772
  });
832
- async function findById(id, opts = {}) {
833
- const found = await findOne(scoped({ _id: id }, opts.withDeleted));
834
- return found ?? undefined;
835
- }
836
- async function getById(id, opts = {}) {
837
- const found = await findById(id, opts);
838
- if (!found)
839
- throw notFound(id);
840
- return found;
841
- }
842
- async function findMany(opts = {}) {
843
- return run(async () => {
844
- let cursor = collection.find(scoped(opts.filter, opts.withDeleted), {
845
- ...sessionOption,
846
- ...opts.projection ? { projection: opts.projection } : {}
847
- });
848
- if (opts.sort !== undefined)
849
- cursor = cursor.sort(opts.sort);
850
- if (opts.skip !== undefined)
851
- cursor = cursor.skip(opts.skip);
852
- if (opts.limit !== undefined)
853
- cursor = cursor.limit(opts.limit);
854
- const found = await cursor.toArray();
855
- return found.map((document) => withId(document));
773
+ }
774
+ async function findById(ctx, id, opts = {}) {
775
+ const found = await findOne(ctx, scoped(ctx, { _id: id }, opts.withDeleted));
776
+ return found ?? undefined;
777
+ }
778
+ async function getById(ctx, id, opts = {}) {
779
+ const found = await findById(ctx, id, opts);
780
+ if (!found)
781
+ throw notFound(ctx, id);
782
+ return found;
783
+ }
784
+ async function findMany(ctx, opts = {}) {
785
+ return run(ctx, async () => {
786
+ let cursor = ctx.collection.find(scoped(ctx, opts.filter, opts.withDeleted), {
787
+ ...ctx.sessionOption,
788
+ ...opts.projection ? { projection: opts.projection } : {}
856
789
  });
790
+ if (opts.sort !== undefined)
791
+ cursor = cursor.sort(opts.sort);
792
+ if (opts.skip !== undefined)
793
+ cursor = cursor.skip(opts.skip);
794
+ if (opts.limit !== undefined)
795
+ cursor = cursor.limit(opts.limit);
796
+ const found = await cursor.toArray();
797
+ return found.map((document) => withId(ctx, document));
798
+ });
799
+ }
800
+ async function findFirst(ctx, filter, opts = {}) {
801
+ const [first] = await findMany(ctx, { ...opts, filter, limit: 1 });
802
+ return first;
803
+ }
804
+ async function countDocuments(ctx, filter, opts = {}) {
805
+ return run(ctx, async () => ctx.collection.countDocuments(scoped(ctx, filter, opts.withDeleted), {
806
+ ...ctx.sessionOption
807
+ }));
808
+ }
809
+ async function exists(ctx, filter, opts = {}) {
810
+ const found = await findOne(ctx, scoped(ctx, filter, opts.withDeleted), {
811
+ _id: 1
812
+ });
813
+ return found !== null && found !== undefined;
814
+ }
815
+
816
+ // src/collection/paginate.ts
817
+ async function paginate(ctx, opts = {}) {
818
+ const window = pageWindow(opts, ctx.maxPageSize);
819
+ const [items, total] = await Promise.all([
820
+ findMany(ctx, {
821
+ filter: opts.filter,
822
+ sort: opts.sort ?? { _id: 1 },
823
+ limit: window.limit,
824
+ skip: window.skip,
825
+ withDeleted: opts.withDeleted
826
+ }),
827
+ countDocuments(ctx, opts.filter, {
828
+ withDeleted: opts.withDeleted
829
+ })
830
+ ]);
831
+ return toPage(items, total, window);
832
+ }
833
+ async function paginateByCursor(ctx, opts = {}) {
834
+ const sortField = opts.orderBy ?? "_id";
835
+ if (!ctx.shape[sortField] && sortField !== "_id") {
836
+ throw new TypeError(`paginateByCursor: "${ctx.name}" has no field "${sortField}" in its schema`);
857
837
  }
858
- async function countDocuments(filter, opts = {}) {
859
- return run(async () => collection.countDocuments(scoped(filter, opts.withDeleted), {
860
- ...sessionOption
861
- }));
862
- }
863
- async function updatedOrThrow(id, filter, update, expectedVersion) {
864
- const updated = await run(async () => collection.findOneAndUpdate(filter, update, {
865
- ...sessionOption,
866
- returnDocument: "after"
867
- }));
868
- if (updated)
869
- return withId(updated);
870
- if (expectedVersion !== undefined) {
871
- const current = await findOne({ _id: id });
872
- if (current) {
873
- throw new OptimisticLockError(`Document ${String(id)} of "${name}" is at version ${String(current.version)}, not ${expectedVersion}: it changed since it was read`, {
874
- collection: name,
875
- id,
876
- expectedVersion,
877
- actualVersion: typeof current.version === "number" ? current.version : undefined
878
- });
879
- }
838
+ const direction = opts.direction ?? "asc";
839
+ const fields = sortField === "_id" ? ["_id"] : [sortField, "_id"];
840
+ const cursorKey = `${sortField}:${direction}`;
841
+ const limit = cursorLimit(opts.limit, ctx.maxPageSize);
842
+ const past = direction === "asc" ? "$gt" : "$lt";
843
+ let after;
844
+ if (opts.after) {
845
+ const { values } = decodeCursor(opts.after, cursorKey);
846
+ if (values.length !== fields.length) {
847
+ throw new DataError(`Invalid cursor: expected ${fields.length} value(s), got ${values.length}`, { collection: ctx.name });
880
848
  }
881
- throw notFound(id);
849
+ after = {
850
+ $or: fields.map((field, index) => ({
851
+ ...Object.fromEntries(fields.slice(0, index).map((previous, i) => [previous, values[i]])),
852
+ [field]: { [past]: values[index] }
853
+ }))
854
+ };
882
855
  }
883
- async function hardDelete(id) {
884
- const deleted = await run(async () => collection.findOneAndDelete({ _id: id }, { ...sessionOption }));
885
- if (!deleted)
886
- throw notFound(id);
887
- return withId(deleted);
856
+ const sort = Object.fromEntries(fields.map((field) => [field, direction === "asc" ? 1 : -1]));
857
+ const documents = await findMany(ctx, {
858
+ filter: mergeFilters(isRecord3(opts.filter) ? opts.filter : undefined, after),
859
+ sort,
860
+ limit: limit + 1,
861
+ withDeleted: opts.withDeleted
862
+ });
863
+ const items = documents.slice(0, limit);
864
+ const last = items.at(-1);
865
+ if (documents.length <= limit || !last) {
866
+ return { items, nextCursor: null };
888
867
  }
889
- async function hardDeleteMany(filter) {
890
- requireFilter("hardDeleteMany", filter);
891
- return run(async () => {
892
- const result = await collection.deleteMany(filter, {
893
- ...sessionOption
868
+ const values = fields.map((field) => {
869
+ const value = last[field];
870
+ if (value === null || value === undefined) {
871
+ throw new TypeError(`paginateByCursor: "${field}" is null in a document of "${ctx.name}". ` + "Page along a field every document has.");
872
+ }
873
+ return value;
874
+ });
875
+ return { items, nextCursor: encodeCursor({ key: cursorKey, values }) };
876
+ }
877
+
878
+ // src/collection/writes.ts
879
+ async function updatedOrThrow(ctx, id, filter, update, expectedVersion) {
880
+ const updated = await run(ctx, async () => ctx.collection.findOneAndUpdate(filter, update, {
881
+ ...ctx.sessionOption,
882
+ returnDocument: "after"
883
+ }));
884
+ if (updated)
885
+ return withId(ctx, updated);
886
+ if (expectedVersion !== undefined) {
887
+ const current = await findOne(ctx, { _id: id });
888
+ if (current) {
889
+ throw new OptimisticLockError(`Document ${String(id)} of "${ctx.name}" is at version ${String(current.version)}, not ${expectedVersion}: it changed since it was read`, {
890
+ collection: ctx.name,
891
+ id,
892
+ expectedVersion,
893
+ actualVersion: typeof current.version === "number" ? current.version : undefined
894
894
  });
895
- return result.deletedCount;
895
+ }
896
+ }
897
+ throw notFound(ctx, id);
898
+ }
899
+ function softDeleteUpdate(ctx) {
900
+ const set = { deletedAt: new Date };
901
+ if (ctx.actor !== undefined && ctx.stamps.deletedBy) {
902
+ set.deletedBy = ctx.actor;
903
+ }
904
+ const update = { $set: set };
905
+ if (ctx.locks)
906
+ update.$inc = { version: 1 };
907
+ return update;
908
+ }
909
+ async function create(ctx, values) {
910
+ const document = toDocument(ctx, values);
911
+ return run(ctx, async () => {
912
+ await ctx.collection.insertOne(document, {
913
+ ...ctx.sessionOption
896
914
  });
915
+ return withId(ctx, document);
916
+ });
917
+ }
918
+ async function createMany(ctx, values) {
919
+ if (values.length === 0)
920
+ return [];
921
+ const documents = values.map((value) => toDocument(ctx, value));
922
+ return run(ctx, async () => {
923
+ await ctx.collection.insertMany(documents, {
924
+ ...ctx.sessionOption
925
+ });
926
+ return documents.map((document) => withId(ctx, document));
927
+ });
928
+ }
929
+ async function update(ctx, id, patch, opts = {}) {
930
+ const expectedVersion = opts.expectedVersion;
931
+ if (expectedVersion !== undefined && !ctx.locks) {
932
+ throw new TypeError(`update: expectedVersion needs a "version" field, and "${ctx.name}" has none`);
897
933
  }
898
- const repository = {
899
- definition,
900
- db,
901
- collection,
902
- session,
903
- with: (other) => build(db, definition, { ...options, session: other }),
904
- as: (who) => build(db, definition, { ...options, actor: who }),
905
- sync: (syncOptions = {}) => syncCollection(db, definition, { ...sessionOption, ...syncOptions }),
906
- findById,
907
- getById,
908
- async findFirst(filter, opts = {}) {
909
- const [first] = await findMany({ ...opts, filter, limit: 1 });
910
- return first;
911
- },
912
- findMany,
913
- async create(values) {
914
- const document = toDocument(values);
915
- return run(async () => {
916
- await collection.insertOne(document, { ...sessionOption });
917
- return withId(document);
918
- });
919
- },
920
- async createMany(values) {
921
- if (values.length === 0)
922
- return [];
923
- const documents = values.map(toDocument);
924
- return run(async () => {
925
- await collection.insertMany(documents, {
926
- ...sessionOption
927
- });
928
- return documents.map((document) => withId(document));
929
- });
930
- },
931
- async update(id, patch, opts = {}) {
932
- const expectedVersion = opts.expectedVersion;
933
- if (expectedVersion !== undefined && !locks) {
934
- throw new TypeError(`update: expectedVersion needs a "version" field, and "${name}" has none`);
935
- }
936
- const update = toUpdate(patch);
937
- const filter = mergeFilters({
938
- _id: id,
939
- ...expectedVersion === undefined ? {} : { version: expectedVersion }
940
- }, live());
941
- return updatedOrThrow(id, filter, update, expectedVersion);
942
- },
943
- async updateMany(filter, patch) {
944
- requireFilter("updateMany", filter);
945
- const update = toUpdate(patch);
946
- return run(async () => {
947
- const result = await collection.updateMany(scoped(filter), update, {
948
- ...sessionOption
949
- });
950
- return result.modifiedCount;
951
- });
952
- },
953
- async delete(id) {
954
- if (!softDeletes)
955
- return hardDelete(id);
956
- const set = { deletedAt: new Date };
957
- if (actor !== undefined && stamps.deletedBy)
958
- set.deletedBy = actor;
959
- const update = { $set: set };
960
- if (locks)
961
- update.$inc = { version: 1 };
962
- return updatedOrThrow(id, mergeFilters({ _id: id }, live()), update, undefined);
963
- },
964
- async deleteMany(filter) {
965
- requireFilter("deleteMany", filter);
966
- if (!softDeletes)
967
- return hardDeleteMany(filter);
968
- const set = { deletedAt: new Date };
969
- if (actor !== undefined && stamps.deletedBy)
970
- set.deletedBy = actor;
971
- const update = { $set: set };
972
- if (locks)
973
- update.$inc = { version: 1 };
974
- return run(async () => {
975
- const result = await collection.updateMany(scoped(filter), update, {
976
- ...sessionOption
977
- });
978
- return result.modifiedCount;
979
- });
980
- },
981
- hardDelete,
982
- hardDeleteMany,
983
- async restore(id) {
984
- if (!stamps.deletedAt) {
985
- throw new TypeError(`restore: "${name}" has no soft delete`);
986
- }
987
- const set = { deletedAt: null };
988
- if (stamps.deletedBy)
989
- set.deletedBy = null;
990
- if (touches)
991
- set.updatedAt = new Date;
992
- const update = { $set: set };
993
- if (locks)
994
- update.$inc = { version: 1 };
995
- return updatedOrThrow(id, { _id: id }, update, undefined);
996
- },
997
- count: countDocuments,
998
- async exists(filter, opts = {}) {
999
- const found = await findOne(scoped(filter, opts.withDeleted), { _id: 1 });
1000
- return found !== null && found !== undefined;
1001
- },
1002
- async paginate(opts = {}) {
1003
- const window = pageWindow(opts, maxPageSize);
1004
- const [items, total] = await Promise.all([
1005
- findMany({
1006
- filter: opts.filter,
1007
- sort: opts.sort ?? { _id: 1 },
1008
- limit: window.limit,
1009
- skip: window.skip,
1010
- withDeleted: opts.withDeleted
1011
- }),
1012
- countDocuments(opts.filter, {
1013
- withDeleted: opts.withDeleted
1014
- })
1015
- ]);
1016
- return toPage(items, total, window);
934
+ const patched = toUpdate(ctx, patch);
935
+ const filter = mergeFilters({
936
+ _id: id,
937
+ ...expectedVersion === undefined ? {} : { version: expectedVersion }
938
+ }, live(ctx));
939
+ return updatedOrThrow(ctx, id, filter, patched, expectedVersion);
940
+ }
941
+ async function updateMany(ctx, filter, patch) {
942
+ requireFilter(ctx, "updateMany", filter);
943
+ const patched = toUpdate(ctx, patch);
944
+ return run(ctx, async () => {
945
+ const result = await ctx.collection.updateMany(scoped(ctx, filter), patched, { ...ctx.sessionOption });
946
+ return result.modifiedCount;
947
+ });
948
+ }
949
+ async function hardDelete(ctx, id) {
950
+ const deleted = await run(ctx, async () => ctx.collection.findOneAndDelete({ _id: id }, { ...ctx.sessionOption }));
951
+ if (!deleted)
952
+ throw notFound(ctx, id);
953
+ return withId(ctx, deleted);
954
+ }
955
+ async function hardDeleteMany(ctx, filter) {
956
+ requireFilter(ctx, "hardDeleteMany", filter);
957
+ return run(ctx, async () => {
958
+ const result = await ctx.collection.deleteMany(filter, {
959
+ ...ctx.sessionOption
960
+ });
961
+ return result.deletedCount;
962
+ });
963
+ }
964
+ async function deleteOne(ctx, id) {
965
+ if (!ctx.softDeletes)
966
+ return hardDelete(ctx, id);
967
+ return updatedOrThrow(ctx, id, mergeFilters({ _id: id }, live(ctx)), softDeleteUpdate(ctx), undefined);
968
+ }
969
+ async function deleteMany(ctx, filter) {
970
+ requireFilter(ctx, "deleteMany", filter);
971
+ if (!ctx.softDeletes)
972
+ return hardDeleteMany(ctx, filter);
973
+ return run(ctx, async () => {
974
+ const result = await ctx.collection.updateMany(scoped(ctx, filter), softDeleteUpdate(ctx), { ...ctx.sessionOption });
975
+ return result.modifiedCount;
976
+ });
977
+ }
978
+ async function restore(ctx, id) {
979
+ if (!ctx.stamps.deletedAt) {
980
+ throw new TypeError(`restore: "${ctx.name}" has no soft delete`);
981
+ }
982
+ const set = { deletedAt: null };
983
+ if (ctx.stamps.deletedBy)
984
+ set.deletedBy = null;
985
+ if (ctx.touches)
986
+ set.updatedAt = new Date;
987
+ const update = { $set: set };
988
+ if (ctx.locks)
989
+ update.$inc = { version: 1 };
990
+ return updatedOrThrow(ctx, id, { _id: id }, update, undefined);
991
+ }
992
+
993
+ // src/collection/get-collection.ts
994
+ function databaseOf(source, name) {
995
+ const client = source;
996
+ if (typeof client.db === "function")
997
+ return client.db(name);
998
+ const db = source;
999
+ if (name !== undefined && db.databaseName !== name) {
1000
+ throw new TypeError(`getCollection: given a Db for "${db.databaseName}", and a db option of ` + `"${name}". Pass the client, or the database you mean.`);
1001
+ }
1002
+ return db;
1003
+ }
1004
+ function getCollection(source, definition, options = {}) {
1005
+ const db = databaseOf(source, options.db);
1006
+ return build(db, definition, options);
1007
+ }
1008
+ function apiOf(ctx, rebuild) {
1009
+ return {
1010
+ definition: ctx.definition,
1011
+ db: ctx.db,
1012
+ raw: ctx.collection,
1013
+ session: ctx.session,
1014
+ withSession: (other) => rebuild({ session: other }),
1015
+ as: (who) => rebuild({ actor: who }),
1016
+ sync: (syncOptions = {}) => syncCollection(ctx.db, ctx.definition, {
1017
+ ...ctx.sessionOption,
1018
+ ...syncOptions
1019
+ }),
1020
+ findById: (id, opts) => findById(ctx, id, opts),
1021
+ getById: (id, opts) => getById(ctx, id, opts),
1022
+ findFirst: (filter, opts) => findFirst(ctx, filter, opts),
1023
+ findMany: (opts) => findMany(ctx, opts),
1024
+ create: (values) => create(ctx, values),
1025
+ createMany: (values) => createMany(ctx, values),
1026
+ update: (id, patch, opts) => update(ctx, id, patch, opts),
1027
+ updateMany: (filter, patch) => updateMany(ctx, filter, patch),
1028
+ delete: (id) => deleteOne(ctx, id),
1029
+ deleteMany: (filter) => deleteMany(ctx, filter),
1030
+ hardDelete: (id) => hardDelete(ctx, id),
1031
+ hardDeleteMany: (filter) => hardDeleteMany(ctx, filter),
1032
+ restore: (id) => restore(ctx, id),
1033
+ count: (filter, opts) => countDocuments(ctx, filter, opts),
1034
+ exists: (filter, opts) => exists(ctx, filter, opts),
1035
+ paginate: (opts) => paginate(ctx, opts),
1036
+ paginateByCursor: (opts) => paginateByCursor(ctx, opts)
1037
+ };
1038
+ }
1039
+ function build(db, definition, options) {
1040
+ const ctx = createContext(db, definition, options);
1041
+ const rebuild = (changed) => build(db, definition, { ...options, ...changed });
1042
+ const api = apiOf(ctx, rebuild);
1043
+ const collection = ctx.collection;
1044
+ return new Proxy(api, {
1045
+ get(target, key, receiver) {
1046
+ if (Reflect.has(target, key))
1047
+ return Reflect.get(target, key, receiver);
1048
+ const value = collection[key];
1049
+ return typeof value === "function" ? value.bind(collection) : value;
1017
1050
  },
1018
- async paginateByCursor(opts = {}) {
1019
- const sortField = opts.orderBy ?? "_id";
1020
- if (!shape[sortField] && sortField !== "_id") {
1021
- throw new TypeError(`paginateByCursor: "${name}" has no field "${sortField}" in its schema`);
1022
- }
1023
- const direction = opts.direction ?? "asc";
1024
- const fields = sortField === "_id" ? ["_id"] : [sortField, "_id"];
1025
- const cursorKey = `${sortField}:${direction}`;
1026
- const limit = cursorLimit(opts.limit, maxPageSize);
1027
- const past = direction === "asc" ? "$gt" : "$lt";
1028
- let after;
1029
- if (opts.after) {
1030
- const { values } = decodeCursor(opts.after, cursorKey);
1031
- if (values.length !== fields.length) {
1032
- throw new DataError(`Invalid cursor: expected ${fields.length} value(s), got ${values.length}`, { collection: name });
1033
- }
1034
- after = {
1035
- $or: fields.map((field, index) => ({
1036
- ...Object.fromEntries(fields.slice(0, index).map((previous, i) => [previous, values[i]])),
1037
- [field]: { [past]: values[index] }
1038
- }))
1039
- };
1040
- }
1041
- const sort = Object.fromEntries(fields.map((field) => [field, direction === "asc" ? 1 : -1]));
1042
- const documents = await findMany({
1043
- filter: mergeFilters(isRecord3(opts.filter) ? opts.filter : undefined, after),
1044
- sort,
1045
- limit: limit + 1,
1046
- withDeleted: opts.withDeleted
1047
- });
1048
- const items = documents.slice(0, limit);
1049
- const last = items.at(-1);
1050
- if (documents.length <= limit || !last) {
1051
- return { items, nextCursor: null };
1052
- }
1053
- const values = fields.map((field) => {
1054
- const value = last[field];
1055
- if (value === null || value === undefined) {
1056
- throw new TypeError(`paginateByCursor: "${field}" is null in a document of "${name}". ` + "Page along a field every document has.");
1057
- }
1058
- return value;
1059
- });
1060
- return { items, nextCursor: encodeCursor({ key: cursorKey, values }) };
1051
+ has(target, key) {
1052
+ return Reflect.has(target, key) || key in collection;
1061
1053
  }
1054
+ });
1055
+ }
1056
+ // src/definition/fields.ts
1057
+ import { ObjectId as ObjectId3 } from "mongodb";
1058
+ import { z as z3 } from "zod";
1059
+
1060
+ // src/definition/object-id.ts
1061
+ import { ObjectId as ObjectId2 } from "mongodb";
1062
+ import { z as z2 } from "zod";
1063
+ var HEX_24 = /^[0-9a-fA-F]{24}$/;
1064
+ function isObjectId2(value) {
1065
+ return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
1066
+ }
1067
+ function isObjectIdString(value) {
1068
+ return typeof value === "string" && HEX_24.test(value);
1069
+ }
1070
+ function isValidObjectId(value) {
1071
+ return isObjectId2(value) || isObjectIdString(value);
1072
+ }
1073
+ function tryObjectId(value) {
1074
+ if (isObjectId2(value))
1075
+ return value;
1076
+ if (isObjectIdString(value))
1077
+ return ObjectId2.createFromHexString(value);
1078
+ return;
1079
+ }
1080
+ function describe(value) {
1081
+ if (value === null)
1082
+ return "null";
1083
+ if (value === undefined)
1084
+ return "undefined";
1085
+ if (typeof value === "string")
1086
+ return `the string ${JSON.stringify(value)}`;
1087
+ return `a ${typeof value}`;
1088
+ }
1089
+ function toObjectId(value, field = "_id") {
1090
+ const made = tryObjectId(value);
1091
+ if (made)
1092
+ return made;
1093
+ throw new InvalidIdError(`${field}: expected an ObjectId or its 24-character hex string, got ${describe(value)}`, { id: value, keys: [field] });
1094
+ }
1095
+ function toObjectIds(values, field = "_id") {
1096
+ return [...values].map((value) => toObjectId(value, field));
1097
+ }
1098
+ function objectIdParam() {
1099
+ return z2.custom(isValidObjectId, {
1100
+ error: "must be an ObjectId or its 24-character hex string"
1101
+ }).transform((value) => toObjectId(value));
1102
+ }
1103
+
1104
+ // src/definition/fields.ts
1105
+ function objectId() {
1106
+ return z3.custom(isObjectId2, { error: "must be an ObjectId" }).meta({ bsonType: "objectId" });
1107
+ }
1108
+ function id() {
1109
+ return objectId().default(() => new ObjectId3);
1110
+ }
1111
+ function timestamps() {
1112
+ return {
1113
+ createdAt: z3.date().default(() => new Date),
1114
+ updatedAt: z3.date().default(() => new Date)
1062
1115
  };
1063
- return repository;
1064
1116
  }
1117
+ function softDelete() {
1118
+ return { deletedAt: z3.date().nullable().default(null) };
1119
+ }
1120
+ function optimisticLock() {
1121
+ return { version: z3.int().nonnegative().default(0) };
1122
+ }
1123
+ function actors(actor = objectId()) {
1124
+ return {
1125
+ createdBy: actor.nullable().default(null),
1126
+ updatedBy: actor.nullable().default(null),
1127
+ deletedBy: actor.nullable().default(null)
1128
+ };
1129
+ }
1130
+ var STAMP_FIELDS = {
1131
+ id: "_id",
1132
+ createdAt: "createdAt",
1133
+ updatedAt: "updatedAt",
1134
+ deletedAt: "deletedAt",
1135
+ version: "version",
1136
+ createdBy: "createdBy",
1137
+ updatedBy: "updatedBy",
1138
+ deletedBy: "deletedBy"
1139
+ };
1065
1140
  // src/transaction/with-transaction.ts
1066
1141
  function isSession(host) {
1067
1142
  return typeof host.inTransaction === "function";
@@ -1100,17 +1175,17 @@ export {
1100
1175
  STAMP_FIELDS,
1101
1176
  ValidationError,
1102
1177
  actors,
1103
- createRepository,
1104
1178
  cursorLimit,
1105
1179
  decodeCursor,
1106
1180
  defineCollection,
1107
1181
  diffIndexes,
1108
1182
  encodeCursor,
1183
+ getCollection,
1109
1184
  hasValidator,
1110
1185
  id,
1111
1186
  indexMatches,
1112
1187
  indexNameOf,
1113
- isObjectId,
1188
+ isObjectId2 as isObjectId,
1114
1189
  isObjectIdString,
1115
1190
  isValidObjectId,
1116
1191
  normalizeIndex,
@@ -1133,5 +1208,5 @@ export {
1133
1208
  withTransaction
1134
1209
  };
1135
1210
 
1136
- //# debugId=5C569B791BADEA5664756E2164756E21
1211
+ //# debugId=AC287DF6F57068F864756E2164756E21
1137
1212
  //# sourceMappingURL=index.js.map