@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/README.md +89 -26
- package/dist/collection/context.d.ts +64 -0
- package/dist/collection/context.d.ts.map +1 -0
- package/dist/collection/documents.d.ts +21 -0
- package/dist/collection/documents.d.ts.map +1 -0
- package/dist/collection/filters.d.ts +15 -0
- package/dist/collection/filters.d.ts.map +1 -0
- package/dist/collection/get-collection.d.ts +26 -0
- package/dist/collection/get-collection.d.ts.map +1 -0
- package/dist/collection/paginate.d.ts +6 -0
- package/dist/collection/paginate.d.ts.map +1 -0
- package/dist/collection/reads.d.ts +19 -0
- package/dist/collection/reads.d.ts.map +1 -0
- package/dist/collection/types.d.ts +270 -0
- package/dist/collection/types.d.ts.map +1 -0
- package/dist/collection/writes.d.ts +12 -0
- package/dist/collection/writes.d.ts.map +1 -0
- package/dist/definition/define-collection.d.ts +2 -2
- package/dist/definition/fields.d.ts +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +664 -589
- package/dist/index.js.map +17 -11
- package/dist/transaction/with-transaction.d.ts +4 -4
- package/package.json +1 -1
- package/dist/repository/create-repository.d.ts +0 -21
- package/dist/repository/create-repository.d.ts.map +0 -1
- package/dist/repository/types.d.ts +0 -147
- package/dist/repository/types.d.ts.map +0 -1
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
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
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
|
|
729
|
-
return
|
|
688
|
+
function live(ctx, withDeleted) {
|
|
689
|
+
return ctx.softDeletes && !withDeleted ? { deletedAt: null } : undefined;
|
|
730
690
|
}
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
704
|
+
}
|
|
705
|
+
Object.defineProperty(document, "id", {
|
|
706
|
+
get: () => String(document._id),
|
|
707
|
+
enumerable: true,
|
|
708
|
+
configurable: true
|
|
774
709
|
});
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
|
|
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 (
|
|
812
|
-
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
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
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
},
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
count: countDocuments,
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
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=
|
|
1211
|
+
//# debugId=AC287DF6F57068F864756E2164756E21
|
|
1137
1212
|
//# sourceMappingURL=index.js.map
|