@nxgt/mongo 0.1.0
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/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/definition/define-collection.d.ts +82 -0
- package/dist/definition/define-collection.d.ts.map +1 -0
- package/dist/definition/fields.d.ts +57 -0
- package/dist/definition/fields.d.ts.map +1 -0
- package/dist/definition/json-schema.d.ts +27 -0
- package/dist/definition/json-schema.d.ts.map +1 -0
- package/dist/errors/data-error.d.ts +92 -0
- package/dist/errors/data-error.d.ts.map +1 -0
- package/dist/errors/to-data-error.d.ts +15 -0
- package/dist/errors/to-data-error.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1057 -0
- package/dist/index.js.map +21 -0
- package/dist/pagination/cursor.d.ts +19 -0
- package/dist/pagination/cursor.d.ts.map +1 -0
- package/dist/pagination/page.d.ts +42 -0
- package/dist/pagination/page.d.ts.map +1 -0
- package/dist/repository/create-repository.d.ts +21 -0
- package/dist/repository/create-repository.d.ts.map +1 -0
- package/dist/repository/types.d.ts +145 -0
- package/dist/repository/types.d.ts.map +1 -0
- package/dist/sync/index-diff.d.ts +38 -0
- package/dist/sync/index-diff.d.ts.map +1 -0
- package/dist/sync/sync-collection.d.ts +62 -0
- package/dist/sync/sync-collection.d.ts.map +1 -0
- package/dist/sync/validator-diff.d.ts +27 -0
- package/dist/sync/validator-diff.d.ts.map +1 -0
- package/dist/transaction/with-transaction.d.ts +34 -0
- package/dist/transaction/with-transaction.d.ts.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1057 @@
|
|
|
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 } from "mongodb";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
function isObjectId(value) {
|
|
32
|
+
return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
|
|
33
|
+
}
|
|
34
|
+
function objectId() {
|
|
35
|
+
return z.custom(isObjectId, { error: "must be an ObjectId" }).meta({ bsonType: "objectId" });
|
|
36
|
+
}
|
|
37
|
+
function id() {
|
|
38
|
+
return objectId().default(() => new ObjectId);
|
|
39
|
+
}
|
|
40
|
+
function timestamps() {
|
|
41
|
+
return {
|
|
42
|
+
createdAt: z.date().default(() => new Date),
|
|
43
|
+
updatedAt: z.date().default(() => new Date)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function softDelete() {
|
|
47
|
+
return { deletedAt: z.date().nullable().default(null) };
|
|
48
|
+
}
|
|
49
|
+
function optimisticLock() {
|
|
50
|
+
return { version: z.int().nonnegative().default(0) };
|
|
51
|
+
}
|
|
52
|
+
function actors(actor = objectId()) {
|
|
53
|
+
return {
|
|
54
|
+
createdBy: actor.nullable().default(null),
|
|
55
|
+
updatedBy: actor.nullable().default(null),
|
|
56
|
+
deletedBy: actor.nullable().default(null)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var STAMP_FIELDS = {
|
|
60
|
+
id: "_id",
|
|
61
|
+
createdAt: "createdAt",
|
|
62
|
+
updatedAt: "updatedAt",
|
|
63
|
+
deletedAt: "deletedAt",
|
|
64
|
+
version: "version",
|
|
65
|
+
createdBy: "createdBy",
|
|
66
|
+
updatedBy: "updatedBy",
|
|
67
|
+
deletedBy: "deletedBy"
|
|
68
|
+
};
|
|
69
|
+
// src/definition/json-schema.ts
|
|
70
|
+
import { z as z2 } from "zod";
|
|
71
|
+
var MONGO_JSON_SCHEMA_KEYWORDS = new Set([
|
|
72
|
+
"additionalItems",
|
|
73
|
+
"additionalProperties",
|
|
74
|
+
"allOf",
|
|
75
|
+
"anyOf",
|
|
76
|
+
"bsonType",
|
|
77
|
+
"dependencies",
|
|
78
|
+
"description",
|
|
79
|
+
"enum",
|
|
80
|
+
"exclusiveMaximum",
|
|
81
|
+
"exclusiveMinimum",
|
|
82
|
+
"items",
|
|
83
|
+
"maxItems",
|
|
84
|
+
"maxLength",
|
|
85
|
+
"maxProperties",
|
|
86
|
+
"maximum",
|
|
87
|
+
"minItems",
|
|
88
|
+
"minLength",
|
|
89
|
+
"minProperties",
|
|
90
|
+
"minimum",
|
|
91
|
+
"multipleOf",
|
|
92
|
+
"not",
|
|
93
|
+
"oneOf",
|
|
94
|
+
"pattern",
|
|
95
|
+
"patternProperties",
|
|
96
|
+
"properties",
|
|
97
|
+
"required",
|
|
98
|
+
"title",
|
|
99
|
+
"type",
|
|
100
|
+
"uniqueItems"
|
|
101
|
+
]);
|
|
102
|
+
var SCHEMA_MAPS = new Set([
|
|
103
|
+
"properties",
|
|
104
|
+
"patternProperties",
|
|
105
|
+
"dependencies"
|
|
106
|
+
]);
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
var INTEGER_BSON_TYPES = ["int", "long", "double"];
|
|
111
|
+
function convertIntegerType(node) {
|
|
112
|
+
const type = node.type;
|
|
113
|
+
if (type === "integer") {
|
|
114
|
+
delete node.type;
|
|
115
|
+
node.bsonType = [...INTEGER_BSON_TYPES];
|
|
116
|
+
node.multipleOf ??= 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(type) && type.includes("integer")) {
|
|
120
|
+
delete node.type;
|
|
121
|
+
node.bsonType = [
|
|
122
|
+
...type.filter((one) => one !== "integer"),
|
|
123
|
+
...INTEGER_BSON_TYPES
|
|
124
|
+
];
|
|
125
|
+
node.multipleOf ??= 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function refName(ref) {
|
|
129
|
+
return ref.replace(/^#\/(definitions|\$defs)\//, "");
|
|
130
|
+
}
|
|
131
|
+
function inline(value, defs, stack) {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.map((one) => inline(one, defs, stack));
|
|
134
|
+
}
|
|
135
|
+
if (!isRecord(value))
|
|
136
|
+
return value;
|
|
137
|
+
if (typeof value.$ref === "string") {
|
|
138
|
+
const name = refName(value.$ref);
|
|
139
|
+
if (stack.includes(name)) {
|
|
140
|
+
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.");
|
|
141
|
+
}
|
|
142
|
+
const target = defs[name];
|
|
143
|
+
if (!isRecord(target)) {
|
|
144
|
+
throw new TypeError(`toMongoJsonSchema: cannot resolve ${value.$ref}, which zod emitted`);
|
|
145
|
+
}
|
|
146
|
+
const { $ref: _ref, ...siblings } = value;
|
|
147
|
+
return {
|
|
148
|
+
...inline(target, defs, [...stack, name]),
|
|
149
|
+
...inline(siblings, defs, stack)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const out = {};
|
|
153
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
154
|
+
if (!MONGO_JSON_SCHEMA_KEYWORDS.has(key))
|
|
155
|
+
continue;
|
|
156
|
+
if (SCHEMA_MAPS.has(key) && isRecord(inner)) {
|
|
157
|
+
const mapped = {};
|
|
158
|
+
for (const [name, schema] of Object.entries(inner)) {
|
|
159
|
+
mapped[name] = inline(schema, defs, stack);
|
|
160
|
+
}
|
|
161
|
+
out[key] = mapped;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
out[key] = inline(inner, defs, stack);
|
|
165
|
+
}
|
|
166
|
+
convertIntegerType(out);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
function toMongoJsonSchema(schema) {
|
|
170
|
+
const json = z2.toJSONSchema(schema, {
|
|
171
|
+
target: "draft-4",
|
|
172
|
+
io: "output",
|
|
173
|
+
unrepresentable: "any",
|
|
174
|
+
override: (ctx) => {
|
|
175
|
+
const type = ctx.zodSchema._zod.def.type;
|
|
176
|
+
if (type === "date" && ctx.jsonSchema.bsonType === undefined) {
|
|
177
|
+
ctx.jsonSchema.bsonType = "date";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const definitions = isRecord(json.definitions) ? json.definitions : isRecord(json.$defs) ? json.$defs : {};
|
|
182
|
+
return inline(json, definitions, []);
|
|
183
|
+
}
|
|
184
|
+
// src/errors/data-error.ts
|
|
185
|
+
class DataError extends Error {
|
|
186
|
+
constructor(message = "Database error", options = {}) {
|
|
187
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
188
|
+
this.name = "DataError";
|
|
189
|
+
this.code = "DATABASE";
|
|
190
|
+
this.collection = options.collection;
|
|
191
|
+
this.id = options.id;
|
|
192
|
+
this.serverCode = options.serverCode;
|
|
193
|
+
this.serverCodeName = options.serverCodeName;
|
|
194
|
+
this.index = options.index;
|
|
195
|
+
this.keys = options.keys ?? [];
|
|
196
|
+
this.values = options.values;
|
|
197
|
+
this.issues = options.issues ?? [];
|
|
198
|
+
this.expectedVersion = options.expectedVersion;
|
|
199
|
+
this.actualVersion = options.actualVersion;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class NotFoundError extends DataError {
|
|
204
|
+
constructor(message = "Not found", options = {}) {
|
|
205
|
+
super(message, options);
|
|
206
|
+
this.name = "NotFoundError";
|
|
207
|
+
this.code = "NOT_FOUND";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class ConflictError extends DataError {
|
|
212
|
+
constructor(message = "Duplicate key", options = {}) {
|
|
213
|
+
super(message, { serverCode: 11000, ...options });
|
|
214
|
+
this.name = "ConflictError";
|
|
215
|
+
this.code = "CONFLICT";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class ValidationError extends DataError {
|
|
220
|
+
constructor(message = "Document failed validation", options = {}) {
|
|
221
|
+
super(message, { serverCode: 121, ...options });
|
|
222
|
+
this.name = "ValidationError";
|
|
223
|
+
this.code = "VALIDATION";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class OptimisticLockError extends DataError {
|
|
228
|
+
constructor(message = "Version conflict", options = {}) {
|
|
229
|
+
super(message, options);
|
|
230
|
+
this.name = "OptimisticLockError";
|
|
231
|
+
this.code = "OPTIMISTIC_LOCK";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
class InvalidCursorError extends DataError {
|
|
236
|
+
constructor(message = "Invalid cursor", options = {}) {
|
|
237
|
+
super(message, options);
|
|
238
|
+
this.name = "InvalidCursorError";
|
|
239
|
+
this.code = "INVALID_CURSOR";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// src/errors/to-data-error.ts
|
|
243
|
+
function isRecord2(value) {
|
|
244
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
245
|
+
}
|
|
246
|
+
function asArray(value) {
|
|
247
|
+
if (Array.isArray(value))
|
|
248
|
+
return value;
|
|
249
|
+
return value === undefined || value === null ? [] : [value];
|
|
250
|
+
}
|
|
251
|
+
function text(value) {
|
|
252
|
+
return typeof value === "string" ? value : undefined;
|
|
253
|
+
}
|
|
254
|
+
function indexFromMessage(message) {
|
|
255
|
+
return message?.match(/index:\s*(\S+)\s+dup key/)?.[1];
|
|
256
|
+
}
|
|
257
|
+
function keysOfDuplicate(error) {
|
|
258
|
+
const pattern = error.keyPattern;
|
|
259
|
+
if (isRecord2(pattern)) {
|
|
260
|
+
const values = isRecord2(error.keyValue) ? error.keyValue : undefined;
|
|
261
|
+
return { keys: Object.keys(pattern), values };
|
|
262
|
+
}
|
|
263
|
+
const inMessage = text(error.errmsg)?.match(/dup key:\s*\{([^}]*)\}/)?.[1];
|
|
264
|
+
if (!inMessage)
|
|
265
|
+
return { keys: [], values: undefined };
|
|
266
|
+
const keys = [...inMessage.matchAll(/([\w.$]+)\s*:/g)].map((match) => match[1]);
|
|
267
|
+
return { keys, values: undefined };
|
|
268
|
+
}
|
|
269
|
+
function firstWriteError(error) {
|
|
270
|
+
for (const write of asArray(error.writeErrors)) {
|
|
271
|
+
const inner = isRecord2(write) && isRecord2(write.err) ? write.err : write;
|
|
272
|
+
if (isRecord2(inner))
|
|
273
|
+
return inner;
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
function issuesOf(details, path = []) {
|
|
278
|
+
const issues = [];
|
|
279
|
+
for (const rule of asArray(details)) {
|
|
280
|
+
if (!isRecord2(rule))
|
|
281
|
+
continue;
|
|
282
|
+
if (rule.propertiesNotSatisfied !== undefined) {
|
|
283
|
+
for (const property of asArray(rule.propertiesNotSatisfied)) {
|
|
284
|
+
if (!isRecord2(property))
|
|
285
|
+
continue;
|
|
286
|
+
const name = text(property.propertyName) ?? "";
|
|
287
|
+
const nested = issuesOf(property.details, [...path, name]);
|
|
288
|
+
const description = text(property.description);
|
|
289
|
+
issues.push(...description === undefined ? nested : nested.map((issue) => ({ description, ...issue })));
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (rule.missingProperties !== undefined) {
|
|
294
|
+
for (const missing of asArray(rule.missingProperties)) {
|
|
295
|
+
issues.push({
|
|
296
|
+
path: [...path, String(missing)].join("."),
|
|
297
|
+
reason: "required",
|
|
298
|
+
specifiedAs: rule.specifiedAs
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (rule.schemaRulesNotSatisfied !== undefined) {
|
|
304
|
+
issues.push(...issuesOf(rule.schemaRulesNotSatisfied, path));
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
issues.push({
|
|
308
|
+
path: path.join("."),
|
|
309
|
+
reason: text(rule.reason) ?? text(rule.operatorName) ?? "invalid",
|
|
310
|
+
...rule.specifiedAs === undefined ? {} : { specifiedAs: rule.specifiedAs },
|
|
311
|
+
...rule.consideredValue === undefined ? {} : { consideredValue: rule.consideredValue },
|
|
312
|
+
...text(rule.consideredType) === undefined ? {} : { consideredType: text(rule.consideredType) }
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return issues;
|
|
316
|
+
}
|
|
317
|
+
function toDataError(error, context = {}) {
|
|
318
|
+
if (error instanceof DataError)
|
|
319
|
+
return error;
|
|
320
|
+
if (!isRecord2(error))
|
|
321
|
+
return error;
|
|
322
|
+
const source = firstWriteError(error) ?? error;
|
|
323
|
+
const code = typeof source.code === "number" ? source.code : typeof error.code === "number" ? error.code : undefined;
|
|
324
|
+
if (typeof code !== "number")
|
|
325
|
+
return error;
|
|
326
|
+
const message = text(source.errmsg) ?? text(source.message) ?? text(error.message) ?? "";
|
|
327
|
+
const common = {
|
|
328
|
+
collection: context.collection,
|
|
329
|
+
serverCode: code,
|
|
330
|
+
serverCodeName: text(error.codeName) ?? text(source.codeName),
|
|
331
|
+
cause: error
|
|
332
|
+
};
|
|
333
|
+
if (code === 11000) {
|
|
334
|
+
const { keys, values } = keysOfDuplicate(source);
|
|
335
|
+
const index = indexFromMessage(message);
|
|
336
|
+
const named = keys.length > 0 ? keys.join(", ") : index ?? "a unique index";
|
|
337
|
+
return new ConflictError(`Duplicate key on ${named}${context.collection ? ` in "${context.collection}"` : ""}`, { ...common, index, keys, values });
|
|
338
|
+
}
|
|
339
|
+
if (code === 121) {
|
|
340
|
+
const errInfo = isRecord2(source.errInfo) ? source.errInfo : undefined;
|
|
341
|
+
const issues = issuesOf(errInfo?.details);
|
|
342
|
+
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) });
|
|
343
|
+
}
|
|
344
|
+
return new DataError(message || `MongoDB error ${code}`, common);
|
|
345
|
+
}
|
|
346
|
+
// src/pagination/cursor.ts
|
|
347
|
+
import { ObjectId as ObjectId2 } from "mongodb";
|
|
348
|
+
function isObjectId2(value) {
|
|
349
|
+
return typeof value === "object" && value !== null && value._bsontype === "ObjectId";
|
|
350
|
+
}
|
|
351
|
+
function replacer(key, value) {
|
|
352
|
+
const raw = this[key];
|
|
353
|
+
if (raw instanceof Date)
|
|
354
|
+
return { $date: raw.toISOString() };
|
|
355
|
+
if (typeof raw === "bigint")
|
|
356
|
+
return { $bigint: raw.toString() };
|
|
357
|
+
if (isObjectId2(raw))
|
|
358
|
+
return { $oid: raw.toHexString() };
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
function reviver(_key, value) {
|
|
362
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
363
|
+
const keys = Object.keys(value);
|
|
364
|
+
if (keys.length === 1) {
|
|
365
|
+
const tagged = value;
|
|
366
|
+
if (typeof tagged.$date === "string")
|
|
367
|
+
return new Date(tagged.$date);
|
|
368
|
+
if (typeof tagged.$bigint === "string")
|
|
369
|
+
return BigInt(tagged.$bigint);
|
|
370
|
+
if (typeof tagged.$oid === "string")
|
|
371
|
+
return new ObjectId2(tagged.$oid);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
function toBase64Url(text) {
|
|
377
|
+
let binary = "";
|
|
378
|
+
for (const byte of new TextEncoder().encode(text)) {
|
|
379
|
+
binary += String.fromCharCode(byte);
|
|
380
|
+
}
|
|
381
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
382
|
+
}
|
|
383
|
+
function fromBase64Url(text) {
|
|
384
|
+
const base64 = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
385
|
+
const binary = atob(base64 + "=".repeat((4 - base64.length % 4) % 4));
|
|
386
|
+
return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
387
|
+
}
|
|
388
|
+
function encodeCursor(payload) {
|
|
389
|
+
return toBase64Url(JSON.stringify([payload.key, payload.values], replacer));
|
|
390
|
+
}
|
|
391
|
+
function decodeCursor(cursor, expectedKey) {
|
|
392
|
+
let parsed;
|
|
393
|
+
try {
|
|
394
|
+
parsed = JSON.parse(fromBase64Url(cursor), reviver);
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
throw new InvalidCursorError("Invalid cursor: it cannot be decoded", {
|
|
397
|
+
cause
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || !Array.isArray(parsed[1])) {
|
|
401
|
+
throw new InvalidCursorError("Invalid cursor: unexpected shape");
|
|
402
|
+
}
|
|
403
|
+
const [key, values] = parsed;
|
|
404
|
+
if (expectedKey !== undefined && key !== expectedKey) {
|
|
405
|
+
throw new InvalidCursorError(`Invalid cursor: it was written for the ordering ${key}, not ${expectedKey}`);
|
|
406
|
+
}
|
|
407
|
+
return { key, values };
|
|
408
|
+
}
|
|
409
|
+
// src/pagination/page.ts
|
|
410
|
+
var DEFAULT_PAGE_SIZE = 20;
|
|
411
|
+
var DEFAULT_MAX_PAGE_SIZE = 100;
|
|
412
|
+
function positiveInteger(name, value) {
|
|
413
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
414
|
+
throw new RangeError(`${name} must be an integer of at least 1, not ${value}`);
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
function pageWindow(options = {}, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
|
|
419
|
+
const page = positiveInteger("page", options.page ?? 1);
|
|
420
|
+
const pageSize = Math.min(positiveInteger("pageSize", options.pageSize ?? DEFAULT_PAGE_SIZE), maxPageSize);
|
|
421
|
+
return { page, pageSize, limit: pageSize, skip: (page - 1) * pageSize };
|
|
422
|
+
}
|
|
423
|
+
function toPage(items, total, window) {
|
|
424
|
+
return {
|
|
425
|
+
items,
|
|
426
|
+
total,
|
|
427
|
+
page: window.page,
|
|
428
|
+
pageSize: window.pageSize,
|
|
429
|
+
pageCount: Math.ceil(total / window.pageSize)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function cursorLimit(limit, maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
|
|
433
|
+
return Math.min(positiveInteger("limit", limit ?? DEFAULT_PAGE_SIZE), maxPageSize);
|
|
434
|
+
}
|
|
435
|
+
// src/sync/index-diff.ts
|
|
436
|
+
var COLLATION_DEFAULTS = {
|
|
437
|
+
caseLevel: false,
|
|
438
|
+
caseFirst: "off",
|
|
439
|
+
strength: 3,
|
|
440
|
+
numericOrdering: false,
|
|
441
|
+
alternate: "non-ignorable",
|
|
442
|
+
maxVariable: "punct",
|
|
443
|
+
normalization: false,
|
|
444
|
+
backwards: false
|
|
445
|
+
};
|
|
446
|
+
var OPTION_DEFAULTS = {
|
|
447
|
+
unique: false,
|
|
448
|
+
sparse: false,
|
|
449
|
+
hidden: false,
|
|
450
|
+
background: false
|
|
451
|
+
};
|
|
452
|
+
var IGNORED = new Set(["v", "ns", "key", "name"]);
|
|
453
|
+
function keyOf(index) {
|
|
454
|
+
const key = index.key;
|
|
455
|
+
return key instanceof Map ? Object.fromEntries(key) : { ...key };
|
|
456
|
+
}
|
|
457
|
+
function indexNameOf(key) {
|
|
458
|
+
return Object.entries(key).map(([field, direction]) => `${field}_${String(direction)}`).join("_");
|
|
459
|
+
}
|
|
460
|
+
function canonicalCollation(value) {
|
|
461
|
+
if (typeof value !== "object" || value === null)
|
|
462
|
+
return value;
|
|
463
|
+
const collation = value;
|
|
464
|
+
const out = {};
|
|
465
|
+
for (const [field, fallback] of Object.entries(COLLATION_DEFAULTS)) {
|
|
466
|
+
out[field] = collation[field] ?? fallback;
|
|
467
|
+
}
|
|
468
|
+
out.locale = collation.locale;
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
function normalizeIndex(index) {
|
|
472
|
+
const key = keyOf(index);
|
|
473
|
+
const options = {};
|
|
474
|
+
for (const [name, value] of Object.entries(index)) {
|
|
475
|
+
if (IGNORED.has(name) || value === undefined)
|
|
476
|
+
continue;
|
|
477
|
+
if (name === "collation") {
|
|
478
|
+
options.collation = canonicalCollation(value);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (OPTION_DEFAULTS[name] === value)
|
|
482
|
+
continue;
|
|
483
|
+
options[name] = value;
|
|
484
|
+
}
|
|
485
|
+
return { name: index.name ?? indexNameOf(key), key, options };
|
|
486
|
+
}
|
|
487
|
+
function canonical(value) {
|
|
488
|
+
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);
|
|
489
|
+
}
|
|
490
|
+
function indexMatches(wanted, live) {
|
|
491
|
+
const a = normalizeIndex(wanted);
|
|
492
|
+
const b = normalizeIndex(live);
|
|
493
|
+
return JSON.stringify(Object.entries(a.key)) === JSON.stringify(Object.entries(b.key)) && canonical(a.options) === canonical(b.options);
|
|
494
|
+
}
|
|
495
|
+
function diffIndexes(wanted, live) {
|
|
496
|
+
const byName = new Map(live.map((index) => [normalizeIndex(index).name, index]));
|
|
497
|
+
const diff = {
|
|
498
|
+
create: [],
|
|
499
|
+
recreate: [],
|
|
500
|
+
unchanged: [],
|
|
501
|
+
extra: []
|
|
502
|
+
};
|
|
503
|
+
const named = new Set;
|
|
504
|
+
for (const index of wanted) {
|
|
505
|
+
const name = normalizeIndex(index).name;
|
|
506
|
+
named.add(name);
|
|
507
|
+
const existing = byName.get(name);
|
|
508
|
+
if (!existing)
|
|
509
|
+
diff.create.push({ ...index, name });
|
|
510
|
+
else if (indexMatches(index, existing))
|
|
511
|
+
diff.unchanged.push(name);
|
|
512
|
+
else
|
|
513
|
+
diff.recreate.push({ ...index, name });
|
|
514
|
+
}
|
|
515
|
+
for (const name of byName.keys()) {
|
|
516
|
+
if (name !== "_id_" && !named.has(name))
|
|
517
|
+
diff.extra.push(name);
|
|
518
|
+
}
|
|
519
|
+
return diff;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/sync/validator-diff.ts
|
|
523
|
+
function canonical2(value) {
|
|
524
|
+
return JSON.stringify(value ?? null, (_name, inner) => inner && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => a < b ? -1 : 1)) : inner);
|
|
525
|
+
}
|
|
526
|
+
function hasValidator(live) {
|
|
527
|
+
return live.validator !== undefined && Object.keys(live.validator).length > 0;
|
|
528
|
+
}
|
|
529
|
+
function validationMatches(wanted, live) {
|
|
530
|
+
if (wanted.validator === undefined)
|
|
531
|
+
return !hasValidator(live);
|
|
532
|
+
if (!hasValidator(live))
|
|
533
|
+
return false;
|
|
534
|
+
return canonical2(live.validator) === canonical2(wanted.validator) && (live.validationLevel ?? "strict") === wanted.level && (live.validationAction ?? "error") === wanted.action;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/sync/sync-collection.ts
|
|
538
|
+
function serverCode(error) {
|
|
539
|
+
const code = error?.code;
|
|
540
|
+
return typeof code === "number" ? code : undefined;
|
|
541
|
+
}
|
|
542
|
+
async function collectionOptions(db, name, session) {
|
|
543
|
+
const [info] = await db.listCollections({ name }, { ...session ? { session } : {}, nameOnly: false }).toArray();
|
|
544
|
+
return info ? info.options ?? {} : undefined;
|
|
545
|
+
}
|
|
546
|
+
async function liveIndexes(db, name, session) {
|
|
547
|
+
try {
|
|
548
|
+
return await db.collection(name).indexes({ session });
|
|
549
|
+
} catch (error) {
|
|
550
|
+
if (serverCode(error) === 26)
|
|
551
|
+
return [];
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function validationFor(definition) {
|
|
556
|
+
const { level, action } = definition.validation;
|
|
557
|
+
return {
|
|
558
|
+
validator: level === "off" ? undefined : { $jsonSchema: toMongoJsonSchema(definition.schema) },
|
|
559
|
+
level,
|
|
560
|
+
action
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function creationOptions(wanted) {
|
|
564
|
+
return wanted.validator === undefined ? {} : {
|
|
565
|
+
validator: wanted.validator,
|
|
566
|
+
validationLevel: wanted.level,
|
|
567
|
+
validationAction: wanted.action
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async function writeValidation(db, name, wanted, session) {
|
|
571
|
+
try {
|
|
572
|
+
await db.command({
|
|
573
|
+
collMod: name,
|
|
574
|
+
validator: wanted.validator ?? {},
|
|
575
|
+
...wanted.validator === undefined ? {} : { validationLevel: wanted.level, validationAction: wanted.action }
|
|
576
|
+
}, session ? { session } : undefined);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (serverCode(error) === 13) {
|
|
579
|
+
throw new DataError(`sync: not allowed to run collMod on "${name}". Writing a validator ` + "needs the `collMod` action, which `readWrite` does not grant and " + "`dbAdmin` does: sync with a role that has it, not with the " + "application’s own user.", { collection: name, serverCode: 13, cause: error });
|
|
580
|
+
}
|
|
581
|
+
throw toDataError(error, { collection: name });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function syncCollection(db, definition, options = {}) {
|
|
585
|
+
const { name } = definition;
|
|
586
|
+
const dryRun = options.dryRun ?? false;
|
|
587
|
+
const session = options.session;
|
|
588
|
+
const wanted = validationFor(definition);
|
|
589
|
+
let live = await collectionOptions(db, name, session);
|
|
590
|
+
let created = false;
|
|
591
|
+
let validator = "unchanged";
|
|
592
|
+
if (!live) {
|
|
593
|
+
created = true;
|
|
594
|
+
if (wanted.validator !== undefined)
|
|
595
|
+
validator = "created";
|
|
596
|
+
if (!dryRun) {
|
|
597
|
+
try {
|
|
598
|
+
await db.createCollection(name, {
|
|
599
|
+
...creationOptions(wanted),
|
|
600
|
+
...session ? { session } : {}
|
|
601
|
+
});
|
|
602
|
+
} catch (error) {
|
|
603
|
+
if (serverCode(error) !== 48) {
|
|
604
|
+
throw toDataError(error, { collection: name });
|
|
605
|
+
}
|
|
606
|
+
created = false;
|
|
607
|
+
validator = "unchanged";
|
|
608
|
+
live = await collectionOptions(db, name, session);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (live && !validationMatches(wanted, live)) {
|
|
613
|
+
validator = wanted.validator === undefined ? "removed" : hasValidator(live) ? "updated" : "created";
|
|
614
|
+
if (!dryRun)
|
|
615
|
+
await writeValidation(db, name, wanted, session);
|
|
616
|
+
}
|
|
617
|
+
const existing = dryRun && created ? [] : await liveIndexes(db, name, session);
|
|
618
|
+
const diff = diffIndexes(definition.indexes, existing);
|
|
619
|
+
const dropped = options.dropUnknownIndexes ? diff.extra : [];
|
|
620
|
+
const build = [...diff.create, ...diff.recreate];
|
|
621
|
+
if (!dryRun) {
|
|
622
|
+
const collection = db.collection(name);
|
|
623
|
+
for (const index of [
|
|
624
|
+
...diff.recreate.map((i) => normalizeIndex(i).name),
|
|
625
|
+
...dropped
|
|
626
|
+
]) {
|
|
627
|
+
await collection.dropIndex(index, session ? { session } : undefined);
|
|
628
|
+
}
|
|
629
|
+
if (build.length > 0) {
|
|
630
|
+
try {
|
|
631
|
+
await collection.createIndexes(build, session ? { session } : undefined);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
throw toDataError(error, { collection: name });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
name,
|
|
639
|
+
created,
|
|
640
|
+
validator,
|
|
641
|
+
indexes: {
|
|
642
|
+
created: diff.create.map((index) => normalizeIndex(index).name),
|
|
643
|
+
recreated: diff.recreate.map((index) => normalizeIndex(index).name),
|
|
644
|
+
dropped,
|
|
645
|
+
unchanged: diff.unchanged
|
|
646
|
+
},
|
|
647
|
+
dryRun
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async function syncCollections(db, definitions, options = {}) {
|
|
651
|
+
const reports = [];
|
|
652
|
+
for (const definition of definitions) {
|
|
653
|
+
reports.push(await syncCollection(db, definition, options));
|
|
654
|
+
}
|
|
655
|
+
return reports;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/repository/create-repository.ts
|
|
659
|
+
function isRecord3(value) {
|
|
660
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
661
|
+
}
|
|
662
|
+
function isUpdateFilter(patch) {
|
|
663
|
+
return Object.keys(patch).some((key) => key.startsWith("$"));
|
|
664
|
+
}
|
|
665
|
+
function mergeFilters(a, b) {
|
|
666
|
+
const left = a && Object.keys(a).length > 0 ? a : undefined;
|
|
667
|
+
const right = b && Object.keys(b).length > 0 ? b : undefined;
|
|
668
|
+
if (!left)
|
|
669
|
+
return right ?? {};
|
|
670
|
+
if (!right)
|
|
671
|
+
return left;
|
|
672
|
+
return { $and: [left, right] };
|
|
673
|
+
}
|
|
674
|
+
function createRepository(db, definition, options = {}) {
|
|
675
|
+
return build(db, definition, options);
|
|
676
|
+
}
|
|
677
|
+
function build(db, definition, options) {
|
|
678
|
+
const name = definition.name;
|
|
679
|
+
const collection = db.collection(name);
|
|
680
|
+
const shape = definition.schema.shape;
|
|
681
|
+
const stamps = stampsOf(definition);
|
|
682
|
+
const session = options.session;
|
|
683
|
+
const actor = options.actor;
|
|
684
|
+
const maxPageSize = options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE;
|
|
685
|
+
const parses = (options.validate ?? "parse") === "parse";
|
|
686
|
+
const softDeletes = options.softDelete ?? stamps.deletedAt;
|
|
687
|
+
const touches = options.touchUpdatedAt ?? stamps.updatedAt;
|
|
688
|
+
const locks = options.optimisticLock ?? stamps.version;
|
|
689
|
+
if (options.softDelete === true && !stamps.deletedAt) {
|
|
690
|
+
throw new TypeError(`createRepository: softDelete needs a "deletedAt" field, and "${name}" has none`);
|
|
691
|
+
}
|
|
692
|
+
if (options.optimisticLock === true && !stamps.version) {
|
|
693
|
+
throw new TypeError(`createRepository: optimisticLock needs a "version" field, and "${name}" has none`);
|
|
694
|
+
}
|
|
695
|
+
const run = async (fn) => {
|
|
696
|
+
try {
|
|
697
|
+
return await fn();
|
|
698
|
+
} catch (error) {
|
|
699
|
+
throw toDataError(error, { collection: name });
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const sessionOption = session ? { session } : {};
|
|
703
|
+
const live = (withDeleted) => softDeletes && !withDeleted ? { deletedAt: null } : undefined;
|
|
704
|
+
const scoped = (filter, withDeleted) => mergeFilters(isRecord3(filter) ? filter : undefined, live(withDeleted));
|
|
705
|
+
const notFound = (id) => new NotFoundError(`No document in "${name}" with _id ${String(id)}`, {
|
|
706
|
+
collection: name,
|
|
707
|
+
id
|
|
708
|
+
});
|
|
709
|
+
const requireFilter = (method, filter) => {
|
|
710
|
+
if (!isRecord3(filter) || Object.keys(filter).length === 0) {
|
|
711
|
+
throw new TypeError(`${method} needs a filter. Pass \`{ _id: { $exists: true } }\` to target every document of "${name}".`);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
const toDocument = (values) => {
|
|
715
|
+
const stamped = { ...values };
|
|
716
|
+
if (actor !== undefined) {
|
|
717
|
+
if (stamps.createdBy && stamped.createdBy === undefined) {
|
|
718
|
+
stamped.createdBy = actor;
|
|
719
|
+
}
|
|
720
|
+
if (stamps.updatedBy && stamped.updatedBy === undefined) {
|
|
721
|
+
stamped.updatedBy = actor;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return parses ? definition.schema.parse(stamped) : stamped;
|
|
725
|
+
};
|
|
726
|
+
const toUpdate = (patch) => {
|
|
727
|
+
if (!isRecord3(patch)) {
|
|
728
|
+
throw new TypeError(`update: expected the document's fields or MongoDB's operators, not ${String(patch)}`);
|
|
729
|
+
}
|
|
730
|
+
const update = isUpdateFilter(patch) ? { ...patch } : {};
|
|
731
|
+
const set = isRecord3(update.$set) ? { ...update.$set } : {};
|
|
732
|
+
if (!isUpdateFilter(patch)) {
|
|
733
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
734
|
+
if (value === undefined)
|
|
735
|
+
continue;
|
|
736
|
+
const schema = shape[field];
|
|
737
|
+
if (!schema) {
|
|
738
|
+
throw new TypeError(`update: "${name}" has no field "${field}" in its schema`);
|
|
739
|
+
}
|
|
740
|
+
set[field] = parses ? schema.parse(value) : value;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (touches && set.updatedAt === undefined)
|
|
744
|
+
set.updatedAt = new Date;
|
|
745
|
+
if (actor !== undefined && stamps.updatedBy && set.updatedBy === undefined) {
|
|
746
|
+
set.updatedBy = actor;
|
|
747
|
+
}
|
|
748
|
+
if (Object.keys(set).length > 0)
|
|
749
|
+
update.$set = set;
|
|
750
|
+
if (locks) {
|
|
751
|
+
const inc = isRecord3(update.$inc) ? { ...update.$inc } : {};
|
|
752
|
+
inc.version = inc.version ?? 1;
|
|
753
|
+
update.$inc = inc;
|
|
754
|
+
}
|
|
755
|
+
return update;
|
|
756
|
+
};
|
|
757
|
+
const findOne = async (filter, projection) => run(async () => collection.findOne(filter, {
|
|
758
|
+
...sessionOption,
|
|
759
|
+
...projection ? { projection } : {}
|
|
760
|
+
}));
|
|
761
|
+
async function findById(id, opts = {}) {
|
|
762
|
+
const found = await findOne(scoped({ _id: id }, opts.withDeleted));
|
|
763
|
+
return found ?? undefined;
|
|
764
|
+
}
|
|
765
|
+
async function getById(id, opts = {}) {
|
|
766
|
+
const found = await findById(id, opts);
|
|
767
|
+
if (!found)
|
|
768
|
+
throw notFound(id);
|
|
769
|
+
return found;
|
|
770
|
+
}
|
|
771
|
+
async function findMany(opts = {}) {
|
|
772
|
+
return run(async () => {
|
|
773
|
+
let cursor = collection.find(scoped(opts.filter, opts.withDeleted), {
|
|
774
|
+
...sessionOption,
|
|
775
|
+
...opts.projection ? { projection: opts.projection } : {}
|
|
776
|
+
});
|
|
777
|
+
if (opts.sort !== undefined)
|
|
778
|
+
cursor = cursor.sort(opts.sort);
|
|
779
|
+
if (opts.skip !== undefined)
|
|
780
|
+
cursor = cursor.skip(opts.skip);
|
|
781
|
+
if (opts.limit !== undefined)
|
|
782
|
+
cursor = cursor.limit(opts.limit);
|
|
783
|
+
return cursor.toArray();
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
async function countDocuments(filter, opts = {}) {
|
|
787
|
+
return run(async () => collection.countDocuments(scoped(filter, opts.withDeleted), {
|
|
788
|
+
...sessionOption
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
async function updatedOrThrow(id, filter, update, expectedVersion) {
|
|
792
|
+
const updated = await run(async () => collection.findOneAndUpdate(filter, update, {
|
|
793
|
+
...sessionOption,
|
|
794
|
+
returnDocument: "after"
|
|
795
|
+
}));
|
|
796
|
+
if (updated)
|
|
797
|
+
return updated;
|
|
798
|
+
if (expectedVersion !== undefined) {
|
|
799
|
+
const current = await findOne({ _id: id });
|
|
800
|
+
if (current) {
|
|
801
|
+
throw new OptimisticLockError(`Document ${String(id)} of "${name}" is at version ${String(current.version)}, not ${expectedVersion}: it changed since it was read`, {
|
|
802
|
+
collection: name,
|
|
803
|
+
id,
|
|
804
|
+
expectedVersion,
|
|
805
|
+
actualVersion: typeof current.version === "number" ? current.version : undefined
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
throw notFound(id);
|
|
810
|
+
}
|
|
811
|
+
async function hardDelete(id) {
|
|
812
|
+
const deleted = await run(async () => collection.findOneAndDelete({ _id: id }, { ...sessionOption }));
|
|
813
|
+
if (!deleted)
|
|
814
|
+
throw notFound(id);
|
|
815
|
+
return deleted;
|
|
816
|
+
}
|
|
817
|
+
async function hardDeleteMany(filter) {
|
|
818
|
+
requireFilter("hardDeleteMany", filter);
|
|
819
|
+
return run(async () => {
|
|
820
|
+
const result = await collection.deleteMany(filter, {
|
|
821
|
+
...sessionOption
|
|
822
|
+
});
|
|
823
|
+
return result.deletedCount;
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
const repository = {
|
|
827
|
+
definition,
|
|
828
|
+
db,
|
|
829
|
+
collection,
|
|
830
|
+
session,
|
|
831
|
+
with: (other) => build(db, definition, { ...options, session: other }),
|
|
832
|
+
as: (who) => build(db, definition, { ...options, actor: who }),
|
|
833
|
+
sync: (syncOptions = {}) => syncCollection(db, definition, { ...sessionOption, ...syncOptions }),
|
|
834
|
+
findById,
|
|
835
|
+
getById,
|
|
836
|
+
async findFirst(filter, opts = {}) {
|
|
837
|
+
const [first] = await findMany({ ...opts, filter, limit: 1 });
|
|
838
|
+
return first;
|
|
839
|
+
},
|
|
840
|
+
findMany,
|
|
841
|
+
async create(values) {
|
|
842
|
+
const document = toDocument(values);
|
|
843
|
+
return run(async () => {
|
|
844
|
+
await collection.insertOne(document, { ...sessionOption });
|
|
845
|
+
return document;
|
|
846
|
+
});
|
|
847
|
+
},
|
|
848
|
+
async createMany(values) {
|
|
849
|
+
if (values.length === 0)
|
|
850
|
+
return [];
|
|
851
|
+
const documents = values.map(toDocument);
|
|
852
|
+
return run(async () => {
|
|
853
|
+
await collection.insertMany(documents, {
|
|
854
|
+
...sessionOption
|
|
855
|
+
});
|
|
856
|
+
return documents;
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
async update(id, patch, opts = {}) {
|
|
860
|
+
const expectedVersion = opts.expectedVersion;
|
|
861
|
+
if (expectedVersion !== undefined && !locks) {
|
|
862
|
+
throw new TypeError(`update: expectedVersion needs a "version" field, and "${name}" has none`);
|
|
863
|
+
}
|
|
864
|
+
const update = toUpdate(patch);
|
|
865
|
+
const filter = mergeFilters({
|
|
866
|
+
_id: id,
|
|
867
|
+
...expectedVersion === undefined ? {} : { version: expectedVersion }
|
|
868
|
+
}, live());
|
|
869
|
+
return updatedOrThrow(id, filter, update, expectedVersion);
|
|
870
|
+
},
|
|
871
|
+
async updateMany(filter, patch) {
|
|
872
|
+
requireFilter("updateMany", filter);
|
|
873
|
+
const update = toUpdate(patch);
|
|
874
|
+
return run(async () => {
|
|
875
|
+
const result = await collection.updateMany(scoped(filter), update, {
|
|
876
|
+
...sessionOption
|
|
877
|
+
});
|
|
878
|
+
return result.modifiedCount;
|
|
879
|
+
});
|
|
880
|
+
},
|
|
881
|
+
async delete(id) {
|
|
882
|
+
if (!softDeletes)
|
|
883
|
+
return hardDelete(id);
|
|
884
|
+
const set = { deletedAt: new Date };
|
|
885
|
+
if (actor !== undefined && stamps.deletedBy)
|
|
886
|
+
set.deletedBy = actor;
|
|
887
|
+
const update = { $set: set };
|
|
888
|
+
if (locks)
|
|
889
|
+
update.$inc = { version: 1 };
|
|
890
|
+
return updatedOrThrow(id, mergeFilters({ _id: id }, live()), update, undefined);
|
|
891
|
+
},
|
|
892
|
+
async deleteMany(filter) {
|
|
893
|
+
requireFilter("deleteMany", filter);
|
|
894
|
+
if (!softDeletes)
|
|
895
|
+
return hardDeleteMany(filter);
|
|
896
|
+
const set = { deletedAt: new Date };
|
|
897
|
+
if (actor !== undefined && stamps.deletedBy)
|
|
898
|
+
set.deletedBy = actor;
|
|
899
|
+
const update = { $set: set };
|
|
900
|
+
if (locks)
|
|
901
|
+
update.$inc = { version: 1 };
|
|
902
|
+
return run(async () => {
|
|
903
|
+
const result = await collection.updateMany(scoped(filter), update, {
|
|
904
|
+
...sessionOption
|
|
905
|
+
});
|
|
906
|
+
return result.modifiedCount;
|
|
907
|
+
});
|
|
908
|
+
},
|
|
909
|
+
hardDelete,
|
|
910
|
+
hardDeleteMany,
|
|
911
|
+
async restore(id) {
|
|
912
|
+
if (!stamps.deletedAt) {
|
|
913
|
+
throw new TypeError(`restore: "${name}" has no soft delete`);
|
|
914
|
+
}
|
|
915
|
+
const set = { deletedAt: null };
|
|
916
|
+
if (stamps.deletedBy)
|
|
917
|
+
set.deletedBy = null;
|
|
918
|
+
if (touches)
|
|
919
|
+
set.updatedAt = new Date;
|
|
920
|
+
const update = { $set: set };
|
|
921
|
+
if (locks)
|
|
922
|
+
update.$inc = { version: 1 };
|
|
923
|
+
return updatedOrThrow(id, { _id: id }, update, undefined);
|
|
924
|
+
},
|
|
925
|
+
count: countDocuments,
|
|
926
|
+
async exists(filter, opts = {}) {
|
|
927
|
+
const found = await findOne(scoped(filter, opts.withDeleted), { _id: 1 });
|
|
928
|
+
return found !== null && found !== undefined;
|
|
929
|
+
},
|
|
930
|
+
async paginate(opts = {}) {
|
|
931
|
+
const window = pageWindow(opts, maxPageSize);
|
|
932
|
+
const [items, total] = await Promise.all([
|
|
933
|
+
findMany({
|
|
934
|
+
filter: opts.filter,
|
|
935
|
+
sort: opts.sort ?? { _id: 1 },
|
|
936
|
+
limit: window.limit,
|
|
937
|
+
skip: window.skip,
|
|
938
|
+
withDeleted: opts.withDeleted
|
|
939
|
+
}),
|
|
940
|
+
countDocuments(opts.filter, {
|
|
941
|
+
withDeleted: opts.withDeleted
|
|
942
|
+
})
|
|
943
|
+
]);
|
|
944
|
+
return toPage(items, total, window);
|
|
945
|
+
},
|
|
946
|
+
async paginateByCursor(opts = {}) {
|
|
947
|
+
const sortField = opts.orderBy ?? "_id";
|
|
948
|
+
if (!shape[sortField] && sortField !== "_id") {
|
|
949
|
+
throw new TypeError(`paginateByCursor: "${name}" has no field "${sortField}" in its schema`);
|
|
950
|
+
}
|
|
951
|
+
const direction = opts.direction ?? "asc";
|
|
952
|
+
const fields = sortField === "_id" ? ["_id"] : [sortField, "_id"];
|
|
953
|
+
const cursorKey = `${sortField}:${direction}`;
|
|
954
|
+
const limit = cursorLimit(opts.limit, maxPageSize);
|
|
955
|
+
const past = direction === "asc" ? "$gt" : "$lt";
|
|
956
|
+
let after;
|
|
957
|
+
if (opts.after) {
|
|
958
|
+
const { values } = decodeCursor(opts.after, cursorKey);
|
|
959
|
+
if (values.length !== fields.length) {
|
|
960
|
+
throw new DataError(`Invalid cursor: expected ${fields.length} value(s), got ${values.length}`, { collection: name });
|
|
961
|
+
}
|
|
962
|
+
after = {
|
|
963
|
+
$or: fields.map((field, index) => ({
|
|
964
|
+
...Object.fromEntries(fields.slice(0, index).map((previous, i) => [previous, values[i]])),
|
|
965
|
+
[field]: { [past]: values[index] }
|
|
966
|
+
}))
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const sort = Object.fromEntries(fields.map((field) => [field, direction === "asc" ? 1 : -1]));
|
|
970
|
+
const documents = await findMany({
|
|
971
|
+
filter: mergeFilters(isRecord3(opts.filter) ? opts.filter : undefined, after),
|
|
972
|
+
sort,
|
|
973
|
+
limit: limit + 1,
|
|
974
|
+
withDeleted: opts.withDeleted
|
|
975
|
+
});
|
|
976
|
+
const items = documents.slice(0, limit);
|
|
977
|
+
const last = items.at(-1);
|
|
978
|
+
if (documents.length <= limit || !last) {
|
|
979
|
+
return { items, nextCursor: null };
|
|
980
|
+
}
|
|
981
|
+
const values = fields.map((field) => {
|
|
982
|
+
const value = last[field];
|
|
983
|
+
if (value === null || value === undefined) {
|
|
984
|
+
throw new TypeError(`paginateByCursor: "${field}" is null in a document of "${name}". ` + "Page along a field every document has.");
|
|
985
|
+
}
|
|
986
|
+
return value;
|
|
987
|
+
});
|
|
988
|
+
return { items, nextCursor: encodeCursor({ key: cursorKey, values }) };
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
return repository;
|
|
992
|
+
}
|
|
993
|
+
// src/transaction/with-transaction.ts
|
|
994
|
+
function isSession(host) {
|
|
995
|
+
return typeof host.inTransaction === "function";
|
|
996
|
+
}
|
|
997
|
+
async function withTransaction(host, fn, options) {
|
|
998
|
+
try {
|
|
999
|
+
if (isSession(host)) {
|
|
1000
|
+
if (host.inTransaction()) {
|
|
1001
|
+
if (options) {
|
|
1002
|
+
throw new TypeError("withTransaction: this session is already in a transaction, which " + "this call joins. MongoDB has no savepoints, so the read " + "concern, the write concern and the read preference are the " + "outer transaction’s.");
|
|
1003
|
+
}
|
|
1004
|
+
return await fn(host);
|
|
1005
|
+
}
|
|
1006
|
+
return await host.withTransaction(fn, options);
|
|
1007
|
+
}
|
|
1008
|
+
const session = host.startSession();
|
|
1009
|
+
try {
|
|
1010
|
+
return await session.withTransaction(fn, options);
|
|
1011
|
+
} finally {
|
|
1012
|
+
await session.endSession();
|
|
1013
|
+
}
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
throw toDataError(error);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
export {
|
|
1019
|
+
ConflictError,
|
|
1020
|
+
DEFAULT_MAX_PAGE_SIZE,
|
|
1021
|
+
DEFAULT_PAGE_SIZE,
|
|
1022
|
+
DataError,
|
|
1023
|
+
InvalidCursorError,
|
|
1024
|
+
MONGO_JSON_SCHEMA_KEYWORDS,
|
|
1025
|
+
NotFoundError,
|
|
1026
|
+
OptimisticLockError,
|
|
1027
|
+
STAMP_FIELDS,
|
|
1028
|
+
ValidationError,
|
|
1029
|
+
actors,
|
|
1030
|
+
createRepository,
|
|
1031
|
+
cursorLimit,
|
|
1032
|
+
decodeCursor,
|
|
1033
|
+
defineCollection,
|
|
1034
|
+
diffIndexes,
|
|
1035
|
+
encodeCursor,
|
|
1036
|
+
hasValidator,
|
|
1037
|
+
id,
|
|
1038
|
+
indexMatches,
|
|
1039
|
+
indexNameOf,
|
|
1040
|
+
normalizeIndex,
|
|
1041
|
+
objectId,
|
|
1042
|
+
optimisticLock,
|
|
1043
|
+
pageWindow,
|
|
1044
|
+
softDelete,
|
|
1045
|
+
stampsOf,
|
|
1046
|
+
syncCollection,
|
|
1047
|
+
syncCollections,
|
|
1048
|
+
timestamps,
|
|
1049
|
+
toDataError,
|
|
1050
|
+
toMongoJsonSchema,
|
|
1051
|
+
toPage,
|
|
1052
|
+
validationMatches,
|
|
1053
|
+
withTransaction
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
//# debugId=44372BBBC1BF50A464756E2164756E21
|
|
1057
|
+
//# sourceMappingURL=index.js.map
|