@nxgt/mongo 0.3.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,29 +1,117 @@
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.");
1
+ // src/definition/json-schema.ts
2
+ import { z } from "zod";
3
+ var MONGO_JSON_SCHEMA_KEYWORDS = new Set([
4
+ "additionalItems",
5
+ "additionalProperties",
6
+ "allOf",
7
+ "anyOf",
8
+ "bsonType",
9
+ "dependencies",
10
+ "description",
11
+ "enum",
12
+ "exclusiveMaximum",
13
+ "exclusiveMinimum",
14
+ "items",
15
+ "maxItems",
16
+ "maxLength",
17
+ "maxProperties",
18
+ "maximum",
19
+ "minItems",
20
+ "minLength",
21
+ "minProperties",
22
+ "minimum",
23
+ "multipleOf",
24
+ "not",
25
+ "oneOf",
26
+ "pattern",
27
+ "patternProperties",
28
+ "properties",
29
+ "required",
30
+ "title",
31
+ "type",
32
+ "uniqueItems"
33
+ ]);
34
+ var SCHEMA_MAPS = new Set([
35
+ "properties",
36
+ "patternProperties",
37
+ "dependencies"
38
+ ]);
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ var INTEGER_BSON_TYPES = ["int", "long", "double"];
43
+ function convertIntegerType(node) {
44
+ const type = node.type;
45
+ if (type === "integer") {
46
+ delete node.type;
47
+ node.bsonType = [...INTEGER_BSON_TYPES];
48
+ node.multipleOf ??= 1;
49
+ return;
50
+ }
51
+ if (Array.isArray(type) && type.includes("integer")) {
52
+ delete node.type;
53
+ node.bsonType = [
54
+ ...type.filter((one) => one !== "integer"),
55
+ ...INTEGER_BSON_TYPES
56
+ ];
57
+ node.multipleOf ??= 1;
5
58
  }
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
59
  }
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
- };
60
+ function refName(ref) {
61
+ return ref.replace(/^#\/(definitions|\$defs)\//, "");
62
+ }
63
+ function inline(value, defs, stack) {
64
+ if (Array.isArray(value)) {
65
+ return value.map((one) => inline(one, defs, stack));
66
+ }
67
+ if (!isRecord(value))
68
+ return value;
69
+ if (typeof value.$ref === "string") {
70
+ const name = refName(value.$ref);
71
+ if (stack.includes(name)) {
72
+ throw new TypeError(`toMongoJsonSchema: "${name}" refers to itself. MongoDB's $jsonSchema ` + "has no $ref, so a recursive schema cannot be a validator. Give the " + "collection no validator, or model the field as an object with no " + "schema of its own.");
73
+ }
74
+ const target = defs[name];
75
+ if (!isRecord(target)) {
76
+ throw new TypeError(`toMongoJsonSchema: cannot resolve ${value.$ref}, which zod emitted`);
77
+ }
78
+ const { $ref: _ref, ...siblings } = value;
79
+ return {
80
+ ...inline(target, defs, [...stack, name]),
81
+ ...inline(siblings, defs, stack)
82
+ };
83
+ }
84
+ const out = {};
85
+ for (const [key, inner] of Object.entries(value)) {
86
+ if (!MONGO_JSON_SCHEMA_KEYWORDS.has(key))
87
+ continue;
88
+ if (SCHEMA_MAPS.has(key) && isRecord(inner)) {
89
+ const mapped = {};
90
+ for (const [name, schema] of Object.entries(inner)) {
91
+ mapped[name] = inline(schema, defs, stack);
92
+ }
93
+ out[key] = mapped;
94
+ continue;
95
+ }
96
+ out[key] = inline(inner, defs, stack);
97
+ }
98
+ convertIntegerType(out);
99
+ return out;
100
+ }
101
+ function toMongoJsonSchema(schema) {
102
+ const json = z.toJSONSchema(schema, {
103
+ target: "draft-4",
104
+ io: "output",
105
+ unrepresentable: "any",
106
+ override: (ctx) => {
107
+ const type = ctx.zodSchema._zod.def.type;
108
+ if (type === "date" && ctx.jsonSchema.bsonType === undefined) {
109
+ ctx.jsonSchema.bsonType = "date";
110
+ }
111
+ }
112
+ });
113
+ const definitions = isRecord(json.definitions) ? json.definitions : isRecord(json.$defs) ? json.$defs : {};
114
+ return inline(json, definitions, []);
27
115
  }
28
116
 
29
117
  // src/errors/data-error.ts
@@ -94,7 +182,7 @@ class InvalidCursorError extends DataError {
94
182
  }
95
183
 
96
184
  // src/errors/to-data-error.ts
97
- function isRecord(value) {
185
+ function isRecord2(value) {
98
186
  return typeof value === "object" && value !== null && !Array.isArray(value);
99
187
  }
100
188
  function asArray(value) {
@@ -110,8 +198,8 @@ function indexFromMessage(message) {
110
198
  }
111
199
  function keysOfDuplicate(error) {
112
200
  const pattern = error.keyPattern;
113
- if (isRecord(pattern)) {
114
- const values = isRecord(error.keyValue) ? error.keyValue : undefined;
201
+ if (isRecord2(pattern)) {
202
+ const values = isRecord2(error.keyValue) ? error.keyValue : undefined;
115
203
  return { keys: Object.keys(pattern), values };
116
204
  }
117
205
  const inMessage = text(error.errmsg)?.match(/dup key:\s*\{([^}]*)\}/)?.[1];
@@ -122,8 +210,8 @@ function keysOfDuplicate(error) {
122
210
  }
123
211
  function firstWriteError(error) {
124
212
  for (const write of asArray(error.writeErrors)) {
125
- const inner = isRecord(write) && isRecord(write.err) ? write.err : write;
126
- if (isRecord(inner))
213
+ const inner = isRecord2(write) && isRecord2(write.err) ? write.err : write;
214
+ if (isRecord2(inner))
127
215
  return inner;
128
216
  }
129
217
  return;
@@ -131,11 +219,11 @@ function firstWriteError(error) {
131
219
  function issuesOf(details, path = []) {
132
220
  const issues = [];
133
221
  for (const rule of asArray(details)) {
134
- if (!isRecord(rule))
222
+ if (!isRecord2(rule))
135
223
  continue;
136
224
  if (rule.propertiesNotSatisfied !== undefined) {
137
225
  for (const property of asArray(rule.propertiesNotSatisfied)) {
138
- if (!isRecord(property))
226
+ if (!isRecord2(property))
139
227
  continue;
140
228
  const name = text(property.propertyName) ?? "";
141
229
  const nested = issuesOf(property.details, [...path, name]);
@@ -171,7 +259,7 @@ function issuesOf(details, path = []) {
171
259
  function toDataError(error, context = {}) {
172
260
  if (error instanceof DataError)
173
261
  return error;
174
- if (!isRecord(error))
262
+ if (!isRecord2(error))
175
263
  return error;
176
264
  const source = firstWriteError(error) ?? error;
177
265
  const code = typeof source.code === "number" ? source.code : typeof error.code === "number" ? error.code : undefined;
@@ -191,303 +279,96 @@ function toDataError(error, context = {}) {
191
279
  return new ConflictError(`Duplicate key on ${named}${context.collection ? ` in "${context.collection}"` : ""}`, { ...common, index, keys, values });
192
280
  }
193
281
  if (code === 121) {
194
- const errInfo = isRecord(source.errInfo) ? source.errInfo : undefined;
282
+ const errInfo = isRecord2(source.errInfo) ? source.errInfo : undefined;
195
283
  const issues = issuesOf(errInfo?.details);
196
284
  return new ValidationError(`Document failed validation${context.collection ? ` in "${context.collection}"` : ""}${issues.length > 0 ? `: ${issues.map((i) => `${i.path} ${i.reason}`).join(", ")}` : ""}`, { ...common, issues, keys: issues.map((issue) => issue.path) });
197
285
  }
198
286
  return new DataError(message || `MongoDB error ${code}`, common);
199
287
  }
200
288
 
201
- // src/pagination/cursor.ts
202
- import { ObjectId } from "mongodb";
203
- function isObjectId(value) {
204
- return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
289
+ // src/sync/index-diff.ts
290
+ var COLLATION_DEFAULTS = {
291
+ caseLevel: false,
292
+ caseFirst: "off",
293
+ strength: 3,
294
+ numericOrdering: false,
295
+ alternate: "non-ignorable",
296
+ maxVariable: "punct",
297
+ normalization: false,
298
+ backwards: false
299
+ };
300
+ var OPTION_DEFAULTS = {
301
+ unique: false,
302
+ sparse: false,
303
+ hidden: false,
304
+ background: false
305
+ };
306
+ var IGNORED = new Set(["v", "ns", "key", "name"]);
307
+ function keyOf(index) {
308
+ const key = index.key;
309
+ return key instanceof Map ? Object.fromEntries(key) : { ...key };
205
310
  }
206
- function replacer(key, value) {
207
- const raw = this[key];
208
- if (raw instanceof Date)
209
- return { $date: raw.toISOString() };
210
- if (typeof raw === "bigint")
211
- return { $bigint: raw.toString() };
212
- if (isObjectId(raw))
213
- return { $oid: raw.toHexString() };
214
- return value;
311
+ function indexNameOf(key) {
312
+ return Object.entries(key).map(([field, direction]) => `${field}_${String(direction)}`).join("_");
215
313
  }
216
- function reviver(_key, value) {
217
- if (value && typeof value === "object" && !Array.isArray(value)) {
218
- const keys = Object.keys(value);
219
- if (keys.length === 1) {
220
- const tagged = value;
221
- if (typeof tagged.$date === "string")
222
- return new Date(tagged.$date);
223
- if (typeof tagged.$bigint === "string")
224
- return BigInt(tagged.$bigint);
225
- if (typeof tagged.$oid === "string")
226
- return new ObjectId(tagged.$oid);
227
- }
314
+ function canonicalCollation(value) {
315
+ if (typeof value !== "object" || value === null)
316
+ return value;
317
+ const collation = value;
318
+ const out = {};
319
+ for (const [field, fallback] of Object.entries(COLLATION_DEFAULTS)) {
320
+ out[field] = collation[field] ?? fallback;
228
321
  }
229
- return value;
322
+ out.locale = collation.locale;
323
+ return out;
230
324
  }
231
- function toBase64Url(text) {
232
- let binary = "";
233
- for (const byte of new TextEncoder().encode(text)) {
234
- binary += String.fromCharCode(byte);
325
+ function normalizeIndex(index) {
326
+ const key = keyOf(index);
327
+ const options = {};
328
+ for (const [name, value] of Object.entries(index)) {
329
+ if (IGNORED.has(name) || value === undefined)
330
+ continue;
331
+ if (name === "collation") {
332
+ options.collation = canonicalCollation(value);
333
+ continue;
334
+ }
335
+ if (OPTION_DEFAULTS[name] === value)
336
+ continue;
337
+ options[name] = value;
235
338
  }
236
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
339
+ return { name: index.name ?? indexNameOf(key), key, options };
237
340
  }
238
- function fromBase64Url(text) {
239
- const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
240
- const binary = atob(base64 + "=".repeat((4 - base64.length % 4) % 4));
241
- return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
341
+ function canonical(value) {
342
+ return JSON.stringify(value, (_name, inner) => inner && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => a < b ? -1 : 1)) : inner);
242
343
  }
243
- function encodeCursor(payload) {
244
- return toBase64Url(JSON.stringify([payload.key, payload.values], replacer));
344
+ function indexMatches(wanted, live) {
345
+ const a = normalizeIndex(wanted);
346
+ const b = normalizeIndex(live);
347
+ return JSON.stringify(Object.entries(a.key)) === JSON.stringify(Object.entries(b.key)) && canonical(a.options) === canonical(b.options);
245
348
  }
246
- function decodeCursor(cursor, expectedKey) {
247
- let parsed;
248
- try {
249
- parsed = JSON.parse(fromBase64Url(cursor), reviver);
250
- } catch (cause) {
251
- throw new InvalidCursorError("Invalid cursor: it cannot be decoded", {
252
- cause
253
- });
349
+ function diffIndexes(wanted, live) {
350
+ const byName = new Map(live.map((index) => [normalizeIndex(index).name, index]));
351
+ const diff = {
352
+ create: [],
353
+ recreate: [],
354
+ unchanged: [],
355
+ extra: []
356
+ };
357
+ const named = new Set;
358
+ for (const index of wanted) {
359
+ const name = normalizeIndex(index).name;
360
+ named.add(name);
361
+ const existing = byName.get(name);
362
+ if (!existing)
363
+ diff.create.push({ ...index, name });
364
+ else if (indexMatches(index, existing))
365
+ diff.unchanged.push(name);
366
+ else
367
+ diff.recreate.push({ ...index, name });
254
368
  }
255
- if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || !Array.isArray(parsed[1])) {
256
- throw new InvalidCursorError("Invalid cursor: unexpected shape");
257
- }
258
- const [key, values] = parsed;
259
- if (expectedKey !== undefined && key !== expectedKey) {
260
- throw new InvalidCursorError(`Invalid cursor: it was written for the ordering ${key}, not ${expectedKey}`);
261
- }
262
- return { key, values };
263
- }
264
-
265
- // src/pagination/page.ts
266
- var DEFAULT_PAGE_SIZE = 20;
267
- var DEFAULT_MAX_PAGE_SIZE = 100;
268
- function positiveInteger(name, value) {
269
- if (!Number.isInteger(value) || value < 1) {
270
- throw new RangeError(`${name} must be an integer of at least 1, not ${value}`);
271
- }
272
- return value;
273
- }
274
- function pageWindow(options = {}, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
275
- const page = positiveInteger("page", options.page ?? 1);
276
- const pageSize = Math.min(positiveInteger("pageSize", options.pageSize ?? DEFAULT_PAGE_SIZE), maxPageSize);
277
- return { page, pageSize, limit: pageSize, skip: (page - 1) * pageSize };
278
- }
279
- function toPage(items, total, window) {
280
- return {
281
- items,
282
- total,
283
- page: window.page,
284
- pageSize: window.pageSize,
285
- pageCount: Math.ceil(total / window.pageSize)
286
- };
287
- }
288
- function cursorLimit(limit, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
289
- return Math.min(positiveInteger("limit", limit ?? DEFAULT_PAGE_SIZE), maxPageSize);
290
- }
291
-
292
- // src/definition/json-schema.ts
293
- import { z } from "zod";
294
- var MONGO_JSON_SCHEMA_KEYWORDS = new Set([
295
- "additionalItems",
296
- "additionalProperties",
297
- "allOf",
298
- "anyOf",
299
- "bsonType",
300
- "dependencies",
301
- "description",
302
- "enum",
303
- "exclusiveMaximum",
304
- "exclusiveMinimum",
305
- "items",
306
- "maxItems",
307
- "maxLength",
308
- "maxProperties",
309
- "maximum",
310
- "minItems",
311
- "minLength",
312
- "minProperties",
313
- "minimum",
314
- "multipleOf",
315
- "not",
316
- "oneOf",
317
- "pattern",
318
- "patternProperties",
319
- "properties",
320
- "required",
321
- "title",
322
- "type",
323
- "uniqueItems"
324
- ]);
325
- var SCHEMA_MAPS = new Set([
326
- "properties",
327
- "patternProperties",
328
- "dependencies"
329
- ]);
330
- function isRecord2(value) {
331
- return typeof value === "object" && value !== null && !Array.isArray(value);
332
- }
333
- var INTEGER_BSON_TYPES = ["int", "long", "double"];
334
- function convertIntegerType(node) {
335
- const type = node.type;
336
- if (type === "integer") {
337
- delete node.type;
338
- node.bsonType = [...INTEGER_BSON_TYPES];
339
- node.multipleOf ??= 1;
340
- return;
341
- }
342
- if (Array.isArray(type) && type.includes("integer")) {
343
- delete node.type;
344
- node.bsonType = [
345
- ...type.filter((one) => one !== "integer"),
346
- ...INTEGER_BSON_TYPES
347
- ];
348
- node.multipleOf ??= 1;
349
- }
350
- }
351
- function refName(ref) {
352
- return ref.replace(/^#\/(definitions|\$defs)\//, "");
353
- }
354
- function inline(value, defs, stack) {
355
- if (Array.isArray(value)) {
356
- return value.map((one) => inline(one, defs, stack));
357
- }
358
- if (!isRecord2(value))
359
- return value;
360
- if (typeof value.$ref === "string") {
361
- const name = refName(value.$ref);
362
- if (stack.includes(name)) {
363
- throw new TypeError(`toMongoJsonSchema: "${name}" refers to itself. MongoDB's $jsonSchema ` + "has no $ref, so a recursive schema cannot be a validator. Give the " + "collection no validator, or model the field as an object with no " + "schema of its own.");
364
- }
365
- const target = defs[name];
366
- if (!isRecord2(target)) {
367
- throw new TypeError(`toMongoJsonSchema: cannot resolve ${value.$ref}, which zod emitted`);
368
- }
369
- const { $ref: _ref, ...siblings } = value;
370
- return {
371
- ...inline(target, defs, [...stack, name]),
372
- ...inline(siblings, defs, stack)
373
- };
374
- }
375
- const out = {};
376
- for (const [key, inner] of Object.entries(value)) {
377
- if (!MONGO_JSON_SCHEMA_KEYWORDS.has(key))
378
- continue;
379
- if (SCHEMA_MAPS.has(key) && isRecord2(inner)) {
380
- const mapped = {};
381
- for (const [name, schema] of Object.entries(inner)) {
382
- mapped[name] = inline(schema, defs, stack);
383
- }
384
- out[key] = mapped;
385
- continue;
386
- }
387
- out[key] = inline(inner, defs, stack);
388
- }
389
- convertIntegerType(out);
390
- return out;
391
- }
392
- function toMongoJsonSchema(schema) {
393
- const json = z.toJSONSchema(schema, {
394
- target: "draft-4",
395
- io: "output",
396
- unrepresentable: "any",
397
- override: (ctx) => {
398
- const type = ctx.zodSchema._zod.def.type;
399
- if (type === "date" && ctx.jsonSchema.bsonType === undefined) {
400
- ctx.jsonSchema.bsonType = "date";
401
- }
402
- }
403
- });
404
- const definitions = isRecord2(json.definitions) ? json.definitions : isRecord2(json.$defs) ? json.$defs : {};
405
- return inline(json, definitions, []);
406
- }
407
-
408
- // src/sync/index-diff.ts
409
- var COLLATION_DEFAULTS = {
410
- caseLevel: false,
411
- caseFirst: "off",
412
- strength: 3,
413
- numericOrdering: false,
414
- alternate: "non-ignorable",
415
- maxVariable: "punct",
416
- normalization: false,
417
- backwards: false
418
- };
419
- var OPTION_DEFAULTS = {
420
- unique: false,
421
- sparse: false,
422
- hidden: false,
423
- background: false
424
- };
425
- var IGNORED = new Set(["v", "ns", "key", "name"]);
426
- function keyOf(index) {
427
- const key = index.key;
428
- return key instanceof Map ? Object.fromEntries(key) : { ...key };
429
- }
430
- function indexNameOf(key) {
431
- return Object.entries(key).map(([field, direction]) => `${field}_${String(direction)}`).join("_");
432
- }
433
- function canonicalCollation(value) {
434
- if (typeof value !== "object" || value === null)
435
- return value;
436
- const collation = value;
437
- const out = {};
438
- for (const [field, fallback] of Object.entries(COLLATION_DEFAULTS)) {
439
- out[field] = collation[field] ?? fallback;
440
- }
441
- out.locale = collation.locale;
442
- return out;
443
- }
444
- function normalizeIndex(index) {
445
- const key = keyOf(index);
446
- const options = {};
447
- for (const [name, value] of Object.entries(index)) {
448
- if (IGNORED.has(name) || value === undefined)
449
- continue;
450
- if (name === "collation") {
451
- options.collation = canonicalCollation(value);
452
- continue;
453
- }
454
- if (OPTION_DEFAULTS[name] === value)
455
- continue;
456
- options[name] = value;
457
- }
458
- return { name: index.name ?? indexNameOf(key), key, options };
459
- }
460
- function canonical(value) {
461
- return JSON.stringify(value, (_name, inner) => inner && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => a < b ? -1 : 1)) : inner);
462
- }
463
- function indexMatches(wanted, live) {
464
- const a = normalizeIndex(wanted);
465
- const b = normalizeIndex(live);
466
- return JSON.stringify(Object.entries(a.key)) === JSON.stringify(Object.entries(b.key)) && canonical(a.options) === canonical(b.options);
467
- }
468
- function diffIndexes(wanted, live) {
469
- const byName = new Map(live.map((index) => [normalizeIndex(index).name, index]));
470
- const diff = {
471
- create: [],
472
- recreate: [],
473
- unchanged: [],
474
- extra: []
475
- };
476
- const named = new Set;
477
- for (const index of wanted) {
478
- const name = normalizeIndex(index).name;
479
- named.add(name);
480
- const existing = byName.get(name);
481
- if (!existing)
482
- diff.create.push({ ...index, name });
483
- else if (indexMatches(index, existing))
484
- diff.unchanged.push(name);
485
- else
486
- diff.recreate.push({ ...index, name });
487
- }
488
- for (const name of byName.keys()) {
489
- if (name !== "_id_" && !named.has(name))
490
- diff.extra.push(name);
369
+ for (const name of byName.keys()) {
370
+ if (name !== "_id_" && !named.has(name))
371
+ diff.extra.push(name);
491
372
  }
492
373
  return diff;
493
374
  }
@@ -628,368 +509,538 @@ async function syncCollections(db, definitions, options = {}) {
628
509
  return reports;
629
510
  }
630
511
 
631
- // src/collection/get-collection.ts
632
- function isRecord3(value) {
633
- return typeof value === "object" && value !== null && !Array.isArray(value);
634
- }
635
- function isUpdateFilter(patch) {
636
- return Object.keys(patch).some((key) => key.startsWith("$"));
637
- }
638
- function mergeFilters(a, b) {
639
- const left = a && Object.keys(a).length > 0 ? a : undefined;
640
- const right = b && Object.keys(b).length > 0 ? b : undefined;
641
- if (!left)
642
- return right ?? {};
643
- if (!right)
644
- return left;
645
- return { $and: [left, right] };
646
- }
647
- function databaseOf(source, name) {
648
- const client = source;
649
- if (typeof client.db === "function")
650
- return client.db(name);
651
- const db = source;
652
- if (name !== undefined && db.databaseName !== name) {
653
- throw new TypeError(`getCollection: given a Db for "${db.databaseName}", and a db option of ` + `"${name}". Pass the client, or the database you mean.`);
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.");
654
516
  }
655
- return db;
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
+ });
656
525
  }
657
- function getCollection(source, definition, options = {}) {
658
- const db = databaseOf(source, options.db);
659
- return build(db, definition, options);
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
+ };
660
538
  }
661
- function build(db, definition, options) {
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) {
662
569
  const name = definition.name;
663
- const collection = db.collection(name);
664
570
  const shape = definition.schema.shape;
665
- const hasOwnId = "id" in shape;
666
571
  const stamps = stampsOf(definition);
667
572
  const session = options.session;
668
- const actor = options.actor;
669
- const maxPageSize = options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE;
670
- const parses = (options.validate ?? "parse") === "parse";
671
- const softDeletes = options.softDelete ?? stamps.deletedAt;
672
- const touches = options.touchUpdatedAt ?? stamps.updatedAt;
673
- const locks = options.optimisticLock ?? stamps.version;
674
573
  if (options.softDelete === true && !stamps.deletedAt) {
675
574
  throw new TypeError(`getCollection: softDelete needs a "deletedAt" field, and "${name}" has none`);
676
575
  }
677
576
  if (options.optimisticLock === true && !stamps.version) {
678
577
  throw new TypeError(`getCollection: optimisticLock needs a "version" field, and "${name}" has none`);
679
578
  }
680
- const run = async (fn) => {
681
- try {
682
- return await fn();
683
- } catch (error) {
684
- throw toDataError(error, { collection: name });
685
- }
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
686
595
  };
687
- const sessionOption = session ? { session } : {};
688
- const live = (withDeleted) => softDeletes && !withDeleted ? { deletedAt: null } : undefined;
689
- const scoped = (filter, withDeleted) => mergeFilters(isRecord3(filter) ? filter : undefined, live(withDeleted));
690
- const withId = (document) => {
691
- if (hasOwnId || !isRecord3(document) || document._id === undefined || Object.hasOwn(document, "id")) {
692
- return document;
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);
693
634
  }
694
- Object.defineProperty(document, "id", {
695
- get: () => String(document._id),
696
- enumerable: true,
697
- configurable: true
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
698
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
673
+ function isRecord3(value) {
674
+ return typeof value === "object" && value !== null && !Array.isArray(value);
675
+ }
676
+ function isUpdateFilter(patch) {
677
+ return Object.keys(patch).some((key) => key.startsWith("$"));
678
+ }
679
+ function mergeFilters(a, b) {
680
+ const left = a && Object.keys(a).length > 0 ? a : undefined;
681
+ const right = b && Object.keys(b).length > 0 ? b : undefined;
682
+ if (!left)
683
+ return right ?? {};
684
+ if (!right)
685
+ return left;
686
+ return { $and: [left, right] };
687
+ }
688
+ function live(ctx, withDeleted) {
689
+ return ctx.softDeletes && !withDeleted ? { deletedAt: null } : undefined;
690
+ }
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}".`);
697
+ }
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")) {
699
703
  return document;
700
- };
701
- const notFound = (id) => new NotFoundError(`No document in "${name}" with _id ${String(id)}`, {
702
- collection: name,
703
- id
704
+ }
705
+ Object.defineProperty(document, "id", {
706
+ get: () => String(document._id),
707
+ enumerable: true,
708
+ configurable: true
704
709
  });
705
- const requireFilter = (method, filter) => {
706
- if (!isRecord3(filter) || Object.keys(filter).length === 0) {
707
- throw new TypeError(`${method} needs a filter. Pass \`{ _id: { $exists: true } }\` to target every document of "${name}".`);
708
- }
709
- };
710
- const toDocument = (values) => {
711
- const stamped = { ...values };
712
- if (!hasOwnId)
713
- delete stamped.id;
714
- if (actor !== undefined) {
715
- if (stamps.createdBy && stamped.createdBy === undefined) {
716
- stamped.createdBy = actor;
717
- }
718
- if (stamps.updatedBy && stamped.updatedBy === undefined) {
719
- stamped.updatedBy = actor;
720
- }
721
- }
722
- return parses ? definition.schema.parse(stamped) : stamped;
723
- };
724
- const toUpdate = (patch) => {
725
- if (!isRecord3(patch)) {
726
- throw new TypeError(`update: expected the document's fields or MongoDB's operators, not ${String(patch)}`);
727
- }
728
- const update = isUpdateFilter(patch) ? { ...patch } : {};
729
- const set = isRecord3(update.$set) ? { ...update.$set } : {};
730
- if (!isUpdateFilter(patch)) {
731
- for (const [field, value] of Object.entries(patch)) {
732
- if (value === undefined)
733
- continue;
734
- const schema = shape[field];
735
- if (!schema) {
736
- throw new TypeError(`update: "${name}" has no field "${field}" in its schema`);
737
- }
738
- set[field] = parses ? schema.parse(value) : value;
739
- }
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;
740
719
  }
741
- if (touches && set.updatedAt === undefined)
742
- set.updatedAt = new Date;
743
- if (actor !== undefined && stamps.updatedBy && set.updatedBy === undefined) {
744
- set.updatedBy = actor;
720
+ if (ctx.stamps.updatedBy && stamped.updatedBy === undefined) {
721
+ stamped.updatedBy = ctx.actor;
745
722
  }
746
- if (Object.keys(set).length > 0)
747
- update.$set = set;
748
- if (locks) {
749
- const inc = isRecord3(update.$inc) ? { ...update.$inc } : {};
750
- inc.version = inc.version ?? 1;
751
- 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`);
752
734
  }
753
- 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)
754
748
  };
755
- const findOne = async (filter, projection) => run(async () => {
756
- const found = await collection.findOne(filter, {
757
- ...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,
758
769
  ...projection ? { projection } : {}
759
770
  });
760
- return found === null ? null : withId(found);
771
+ return found === null ? null : withId(ctx, found);
761
772
  });
762
- async function findById(id, opts = {}) {
763
- const found = await findOne(scoped({ _id: id }, opts.withDeleted));
764
- return found ?? undefined;
765
- }
766
- async function getById(id, opts = {}) {
767
- const found = await findById(id, opts);
768
- if (!found)
769
- throw notFound(id);
770
- return found;
771
- }
772
- async function findMany(opts = {}) {
773
- return run(async () => {
774
- let cursor = collection.find(scoped(opts.filter, opts.withDeleted), {
775
- ...sessionOption,
776
- ...opts.projection ? { projection: opts.projection } : {}
777
- });
778
- if (opts.sort !== undefined)
779
- cursor = cursor.sort(opts.sort);
780
- if (opts.skip !== undefined)
781
- cursor = cursor.skip(opts.skip);
782
- if (opts.limit !== undefined)
783
- cursor = cursor.limit(opts.limit);
784
- const found = await cursor.toArray();
785
- 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 } : {}
786
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`);
787
837
  }
788
- async function countDocuments(filter, opts = {}) {
789
- return run(async () => collection.countDocuments(scoped(filter, opts.withDeleted), {
790
- ...sessionOption
791
- }));
792
- }
793
- async function updatedOrThrow(id, filter, update, expectedVersion) {
794
- const updated = await run(async () => collection.findOneAndUpdate(filter, update, {
795
- ...sessionOption,
796
- returnDocument: "after"
797
- }));
798
- if (updated)
799
- return withId(updated);
800
- if (expectedVersion !== undefined) {
801
- const current = await findOne({ _id: id });
802
- if (current) {
803
- throw new OptimisticLockError(`Document ${String(id)} of "${name}" is at version ${String(current.version)}, not ${expectedVersion}: it changed since it was read`, {
804
- collection: name,
805
- id,
806
- expectedVersion,
807
- actualVersion: typeof current.version === "number" ? current.version : undefined
808
- });
809
- }
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 });
810
848
  }
811
- 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
+ };
812
855
  }
813
- async function hardDelete(id) {
814
- const deleted = await run(async () => collection.findOneAndDelete({ _id: id }, { ...sessionOption }));
815
- if (!deleted)
816
- throw notFound(id);
817
- 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 };
818
867
  }
819
- async function hardDeleteMany(filter) {
820
- requireFilter("hardDeleteMany", filter);
821
- return run(async () => {
822
- const result = await collection.deleteMany(filter, {
823
- ...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
824
894
  });
825
- 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
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
826
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`);
827
933
  }
828
- const api = {
829
- definition,
830
- db,
831
- raw: collection,
832
- session,
833
- withSession: (other) => build(db, definition, { ...options, session: other }),
834
- as: (who) => build(db, definition, { ...options, actor: who }),
835
- sync: (syncOptions = {}) => syncCollection(db, definition, { ...sessionOption, ...syncOptions }),
836
- findById,
837
- getById,
838
- async findFirst(filter, opts = {}) {
839
- const [first] = await findMany({ ...opts, filter, limit: 1 });
840
- return first;
841
- },
842
- findMany,
843
- async create(values) {
844
- const document = toDocument(values);
845
- return run(async () => {
846
- await collection.insertOne(document, { ...sessionOption });
847
- return withId(document);
848
- });
849
- },
850
- async createMany(values) {
851
- if (values.length === 0)
852
- return [];
853
- const documents = values.map(toDocument);
854
- return run(async () => {
855
- await collection.insertMany(documents, {
856
- ...sessionOption
857
- });
858
- return documents.map((document) => withId(document));
859
- });
860
- },
861
- async update(id, patch, opts = {}) {
862
- const expectedVersion = opts.expectedVersion;
863
- if (expectedVersion !== undefined && !locks) {
864
- throw new TypeError(`update: expectedVersion needs a "version" field, and "${name}" has none`);
865
- }
866
- const update = toUpdate(patch);
867
- const filter = mergeFilters({
868
- _id: id,
869
- ...expectedVersion === undefined ? {} : { version: expectedVersion }
870
- }, live());
871
- return updatedOrThrow(id, filter, update, expectedVersion);
872
- },
873
- async updateMany(filter, patch) {
874
- requireFilter("updateMany", filter);
875
- const update = toUpdate(patch);
876
- return run(async () => {
877
- const result = await collection.updateMany(scoped(filter), update, {
878
- ...sessionOption
879
- });
880
- return result.modifiedCount;
881
- });
882
- },
883
- async delete(id) {
884
- if (!softDeletes)
885
- return hardDelete(id);
886
- const set = { deletedAt: new Date };
887
- if (actor !== undefined && stamps.deletedBy)
888
- set.deletedBy = actor;
889
- const update = { $set: set };
890
- if (locks)
891
- update.$inc = { version: 1 };
892
- return updatedOrThrow(id, mergeFilters({ _id: id }, live()), update, undefined);
893
- },
894
- async deleteMany(filter) {
895
- requireFilter("deleteMany", filter);
896
- if (!softDeletes)
897
- return hardDeleteMany(filter);
898
- const set = { deletedAt: new Date };
899
- if (actor !== undefined && stamps.deletedBy)
900
- set.deletedBy = actor;
901
- const update = { $set: set };
902
- if (locks)
903
- update.$inc = { version: 1 };
904
- return run(async () => {
905
- const result = await collection.updateMany(scoped(filter), update, {
906
- ...sessionOption
907
- });
908
- return result.modifiedCount;
909
- });
910
- },
911
- hardDelete,
912
- hardDeleteMany,
913
- async restore(id) {
914
- if (!stamps.deletedAt) {
915
- throw new TypeError(`restore: "${name}" has no soft delete`);
916
- }
917
- const set = { deletedAt: null };
918
- if (stamps.deletedBy)
919
- set.deletedBy = null;
920
- if (touches)
921
- set.updatedAt = new Date;
922
- const update = { $set: set };
923
- if (locks)
924
- update.$inc = { version: 1 };
925
- return updatedOrThrow(id, { _id: id }, update, undefined);
926
- },
927
- count: countDocuments,
928
- async exists(filter, opts = {}) {
929
- const found = await findOne(scoped(filter, opts.withDeleted), { _id: 1 });
930
- return found !== null && found !== undefined;
931
- },
932
- async paginate(opts = {}) {
933
- const window = pageWindow(opts, maxPageSize);
934
- const [items, total] = await Promise.all([
935
- findMany({
936
- filter: opts.filter,
937
- sort: opts.sort ?? { _id: 1 },
938
- limit: window.limit,
939
- skip: window.skip,
940
- withDeleted: opts.withDeleted
941
- }),
942
- countDocuments(opts.filter, {
943
- withDeleted: opts.withDeleted
944
- })
945
- ]);
946
- return toPage(items, total, window);
947
- },
948
- async paginateByCursor(opts = {}) {
949
- const sortField = opts.orderBy ?? "_id";
950
- if (!shape[sortField] && sortField !== "_id") {
951
- throw new TypeError(`paginateByCursor: "${name}" has no field "${sortField}" in its schema`);
952
- }
953
- const direction = opts.direction ?? "asc";
954
- const fields = sortField === "_id" ? ["_id"] : [sortField, "_id"];
955
- const cursorKey = `${sortField}:${direction}`;
956
- const limit = cursorLimit(opts.limit, maxPageSize);
957
- const past = direction === "asc" ? "$gt" : "$lt";
958
- let after;
959
- if (opts.after) {
960
- const { values } = decodeCursor(opts.after, cursorKey);
961
- if (values.length !== fields.length) {
962
- throw new DataError(`Invalid cursor: expected ${fields.length} value(s), got ${values.length}`, { collection: name });
963
- }
964
- after = {
965
- $or: fields.map((field, index) => ({
966
- ...Object.fromEntries(fields.slice(0, index).map((previous, i) => [previous, values[i]])),
967
- [field]: { [past]: values[index] }
968
- }))
969
- };
970
- }
971
- const sort = Object.fromEntries(fields.map((field) => [field, direction === "asc" ? 1 : -1]));
972
- const documents = await findMany({
973
- filter: mergeFilters(isRecord3(opts.filter) ? opts.filter : undefined, after),
974
- sort,
975
- limit: limit + 1,
976
- withDeleted: opts.withDeleted
977
- });
978
- const items = documents.slice(0, limit);
979
- const last = items.at(-1);
980
- if (documents.length <= limit || !last) {
981
- return { items, nextCursor: null };
982
- }
983
- const values = fields.map((field) => {
984
- const value = last[field];
985
- if (value === null || value === undefined) {
986
- throw new TypeError(`paginateByCursor: "${field}" is null in a document of "${name}". ` + "Page along a field every document has.");
987
- }
988
- return value;
989
- });
990
- return { items, nextCursor: encodeCursor({ key: cursorKey, values }) };
991
- }
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)
992
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;
993
1044
  return new Proxy(api, {
994
1045
  get(target, key, receiver) {
995
1046
  if (Reflect.has(target, key))
@@ -1157,5 +1208,5 @@ export {
1157
1208
  withTransaction
1158
1209
  };
1159
1210
 
1160
- //# debugId=3B847E76D4AAF66F64756E2164756E21
1211
+ //# debugId=AC287DF6F57068F864756E2164756E21
1161
1212
  //# sourceMappingURL=index.js.map