@ignex/ninox 0.1.6 → 0.2.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/README.md +46 -22
- package/dist/index.d.ts +133 -102
- package/dist/index.js +840 -247
- package/dist/index.js.map +1 -1
- package/dist/{timeout-B10CTiGm.d.ts → timeout-BaP92Iyg.d.ts} +5 -15
- package/dist/utils.d.ts +4 -15
- package/dist/utils.js +91 -45
- package/dist/utils.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ObjectId, Decimal128, Long, Int32, Double, Timestamp, Binary, MinKey, MaxKey, DBRef, Code, MongoClient } from 'mongodb';
|
|
1
|
+
import { BSON, ObjectId, Decimal128, Long, Int32, Double, Timestamp, Binary, MinKey, MaxKey, DBRef, Code, MongoClient } from 'mongodb';
|
|
2
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
3
|
import { writeFile, mkdir, readdir } from 'fs/promises';
|
|
3
4
|
import { basename, join } from 'path';
|
|
4
5
|
import { pathToFileURL } from 'url';
|
|
6
|
+
import { isIP } from 'net';
|
|
5
7
|
|
|
6
8
|
// src/utils/timeout.ts
|
|
7
9
|
var withTimeout = (promise, timeoutMs, label) => {
|
|
@@ -57,6 +59,17 @@ var probeMongoCapabilities = async (client) => {
|
|
|
57
59
|
}
|
|
58
60
|
};
|
|
59
61
|
|
|
62
|
+
// src/utils/redact.ts
|
|
63
|
+
var CONNECTION_CREDENTIALS = /([a-z][a-z0-9+.-]*:\/\/[^/\s:@]*):[^/\s@]*@/gi;
|
|
64
|
+
var SECRET_PAIR = /\b(password|passwd|pwd|secret|token|api[-_]?key|access[-_]?key)=([^&\s;,"')]+)/gi;
|
|
65
|
+
var maskCredentials = (value) => value.replace(CONNECTION_CREDENTIALS, "$1:***@");
|
|
66
|
+
var maskSecretPairs = (value) => value.replace(SECRET_PAIR, "$1=***");
|
|
67
|
+
var redactText = (value, max = 300) => {
|
|
68
|
+
const single = maskSecretPairs(maskCredentials(value)).split("\n").find((line) => line.trim().length > 0);
|
|
69
|
+
const text = (single ?? "").trim();
|
|
70
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
71
|
+
};
|
|
72
|
+
|
|
60
73
|
// src/errors/classes.ts
|
|
61
74
|
var ERROR_HTTP_STATUS = {
|
|
62
75
|
BAD_REQUEST: 400,
|
|
@@ -68,22 +81,49 @@ var ERROR_HTTP_STATUS = {
|
|
|
68
81
|
VALIDATION_FAILED: 422,
|
|
69
82
|
MONGO_TIMEOUT: 504
|
|
70
83
|
};
|
|
84
|
+
var STATUS_MESSAGES = {
|
|
85
|
+
400: "Bad Request",
|
|
86
|
+
401: "Unauthorized",
|
|
87
|
+
403: "Forbidden",
|
|
88
|
+
404: "Not Found",
|
|
89
|
+
409: "Conflict",
|
|
90
|
+
422: "Unprocessable Entity",
|
|
91
|
+
429: "Too Many Requests",
|
|
92
|
+
500: "Internal Server Error",
|
|
93
|
+
502: "Bad Gateway",
|
|
94
|
+
503: "Service Unavailable",
|
|
95
|
+
504: "Gateway Timeout"
|
|
96
|
+
};
|
|
97
|
+
var genericStatusMessage = (statusCode) => STATUS_MESSAGES[statusCode] ?? (statusCode >= 500 ? "Internal Server Error" : "Request failed");
|
|
71
98
|
var AppError = class extends Error {
|
|
72
99
|
code;
|
|
73
100
|
statusCode;
|
|
74
101
|
extra;
|
|
75
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Whether `message`/`extra` may be sent to a CLIENT.
|
|
104
|
+
*
|
|
105
|
+
* Fail-closed: a 4xx message is the caller's contract, a 5xx message is an
|
|
106
|
+
* operator detail that may quote a driver, a host or a query. Pass `expose`
|
|
107
|
+
* explicitly to override — only for a 5xx whose text was written for callers
|
|
108
|
+
* (e.g. `new InfraError("DRAINING", "queue is draining", extra, true)`).
|
|
109
|
+
*/
|
|
110
|
+
expose;
|
|
111
|
+
constructor(code, message, statusCode = 500, extra, expose) {
|
|
76
112
|
super(message);
|
|
77
113
|
this.name = "AppError";
|
|
78
114
|
this.code = code;
|
|
79
115
|
this.statusCode = statusCode;
|
|
116
|
+
this.expose = expose ?? statusCode < 500;
|
|
80
117
|
if (extra !== void 0) this.extra = extra;
|
|
81
118
|
}
|
|
82
119
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
120
|
+
* Full, stack-free shape for LOGS and typed consumers:
|
|
121
|
+
* `{ name, code, message, statusCode, ...(extra) }`.
|
|
122
|
+
*
|
|
123
|
+
* It carries the real message, so it is NOT the client path — use
|
|
124
|
+
* {@link toClientPayload} (or `serializeError`) for anything a caller sees, and
|
|
125
|
+
* prefer `logger.error({ err })` so pino's own serializer keeps the stack in the
|
|
126
|
+
* log rather than in a payload.
|
|
87
127
|
*/
|
|
88
128
|
toJSON() {
|
|
89
129
|
return {
|
|
@@ -94,22 +134,42 @@ var AppError = class extends Error {
|
|
|
94
134
|
...this.extra !== void 0 ? { extra: this.extra } : {}
|
|
95
135
|
};
|
|
96
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The client-safe payload — the same shape as {@link toJSON}, except that a
|
|
139
|
+
* non-exposed error's `message` is replaced by the canonical reason phrase and
|
|
140
|
+
* its `extra` is dropped. A framework can forward this (or `serializeError`'s
|
|
141
|
+
* result) without auditing what the message might contain.
|
|
142
|
+
*
|
|
143
|
+
* @param options - `expose: true` forces the real (masked) message + extra.
|
|
144
|
+
* @returns The client payload.
|
|
145
|
+
*/
|
|
146
|
+
toClientPayload(options = {}) {
|
|
147
|
+
const expose = options.expose === true || this.expose;
|
|
148
|
+
const payload = {
|
|
149
|
+
name: this.name,
|
|
150
|
+
code: this.code,
|
|
151
|
+
message: expose ? maskCredentials(this.message) : genericStatusMessage(this.statusCode),
|
|
152
|
+
statusCode: this.statusCode
|
|
153
|
+
};
|
|
154
|
+
if (expose && this.extra !== void 0) payload.extra = this.extra;
|
|
155
|
+
return payload;
|
|
156
|
+
}
|
|
97
157
|
};
|
|
98
158
|
var DomainError = class extends AppError {
|
|
99
|
-
constructor(code, message, extra) {
|
|
100
|
-
super(code, message, ERROR_HTTP_STATUS[code] ?? 400, extra);
|
|
159
|
+
constructor(code, message, extra, expose) {
|
|
160
|
+
super(code, message, ERROR_HTTP_STATUS[code] ?? 400, extra, expose);
|
|
101
161
|
this.name = "DomainError";
|
|
102
162
|
}
|
|
103
163
|
};
|
|
104
164
|
var InfraError = class extends AppError {
|
|
105
|
-
constructor(code, message, extra) {
|
|
106
|
-
super(code, message, ERROR_HTTP_STATUS[code] ?? 500, extra);
|
|
165
|
+
constructor(code, message, extra, expose) {
|
|
166
|
+
super(code, message, ERROR_HTTP_STATUS[code] ?? 500, extra, expose);
|
|
107
167
|
this.name = "InfraError";
|
|
108
168
|
}
|
|
109
169
|
};
|
|
110
170
|
var BadRequest = class extends DomainError {
|
|
111
|
-
constructor(message, extra) {
|
|
112
|
-
super("BAD_REQUEST", message, extra);
|
|
171
|
+
constructor(message, extra, expose) {
|
|
172
|
+
super("BAD_REQUEST", message, extra, expose);
|
|
113
173
|
this.name = "BadRequest";
|
|
114
174
|
}
|
|
115
175
|
};
|
|
@@ -170,7 +230,7 @@ var isMongoDuplicateKeyError = (error) => {
|
|
|
170
230
|
};
|
|
171
231
|
var isTransactionUnsupportedError = (error) => {
|
|
172
232
|
const code = error?.code;
|
|
173
|
-
if (code === 20) return true;
|
|
233
|
+
if (code === 20 || code === "TRANSACTIONS_UNSUPPORTED") return true;
|
|
174
234
|
const message = error instanceof Error ? error.message : "";
|
|
175
235
|
return /Transaction numbers are only allowed/i.test(message) || /transactions are not supported/i.test(message) || /does not support retryable writes/i.test(message);
|
|
176
236
|
};
|
|
@@ -255,6 +315,12 @@ var mapMongoDriverError = (error, ctx = {}) => {
|
|
|
255
315
|
}
|
|
256
316
|
const code = error.code;
|
|
257
317
|
switch (code) {
|
|
318
|
+
case 20:
|
|
319
|
+
return new InfraError(
|
|
320
|
+
"TRANSACTIONS_UNSUPPORTED",
|
|
321
|
+
"MongoDB deployment does not support transactions; enable allowNonTransactionalFallback to run non-atomically",
|
|
322
|
+
context
|
|
323
|
+
);
|
|
258
324
|
case 112:
|
|
259
325
|
return new DomainError("VERSION_CONFLICT", "Document version conflict", context);
|
|
260
326
|
case 50:
|
|
@@ -279,13 +345,20 @@ var httpStatusForError = (err) => {
|
|
|
279
345
|
if (isMongoTransientError(err)) return 503;
|
|
280
346
|
return 500;
|
|
281
347
|
};
|
|
282
|
-
var serializeError = (err, ctx = {}) => {
|
|
348
|
+
var serializeError = (err, ctx = {}, options = {}) => {
|
|
283
349
|
const mapped = mapMongoDriverError(err, ctx);
|
|
284
|
-
if (mapped instanceof AppError) return mapped.
|
|
285
|
-
|
|
286
|
-
|
|
350
|
+
if (mapped instanceof AppError) return mapped.toClientPayload(options);
|
|
351
|
+
const statusCode = httpStatusForError(mapped);
|
|
352
|
+
const name = mapped instanceof Error ? mapped.name : "UnknownError";
|
|
353
|
+
if (options.expose !== true) {
|
|
354
|
+
return { name, code: "UNKNOWN", message: genericStatusMessage(statusCode), statusCode };
|
|
287
355
|
}
|
|
288
|
-
return {
|
|
356
|
+
return {
|
|
357
|
+
name,
|
|
358
|
+
code: "UNKNOWN",
|
|
359
|
+
message: redactText(mapped instanceof Error ? mapped.message : String(mapped)),
|
|
360
|
+
statusCode
|
|
361
|
+
};
|
|
289
362
|
};
|
|
290
363
|
var cloneDeep = (value) => {
|
|
291
364
|
if (value instanceof Date) return new Date(value.getTime());
|
|
@@ -341,46 +414,81 @@ var cloneDeep = (value) => {
|
|
|
341
414
|
}
|
|
342
415
|
return value;
|
|
343
416
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (v instanceof Date) return `date:${v.getTime()}`;
|
|
354
|
-
if (typeof v === "object") {
|
|
355
|
-
const anyV = v;
|
|
356
|
-
if (typeof anyV.toHexString === "function") return `oid:${anyV.toHexString()}`;
|
|
357
|
-
if (anyV instanceof RegExp) return `re:${anyV.source}/${anyV.flags}`;
|
|
358
|
-
}
|
|
359
|
-
return v;
|
|
417
|
+
var { EJSON } = BSON;
|
|
418
|
+
var bsonTypeOf = (value) => {
|
|
419
|
+
const type = value._bsontype;
|
|
420
|
+
return typeof type === "string" ? type : void 0;
|
|
421
|
+
};
|
|
422
|
+
var isPlainObject = (value) => {
|
|
423
|
+
if (value === null || typeof value !== "object") return false;
|
|
424
|
+
const prototype = Object.getPrototypeOf(value);
|
|
425
|
+
return (prototype === Object.prototype || prototype === null) && bsonTypeOf(value) === void 0;
|
|
360
426
|
};
|
|
361
427
|
var stableStringify = (value) => {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
428
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
429
|
+
const visit = (input) => {
|
|
430
|
+
if (input === null) return { t: "null" };
|
|
431
|
+
if (input === void 0) return { t: "undefined" };
|
|
432
|
+
switch (typeof input) {
|
|
433
|
+
case "string":
|
|
434
|
+
case "boolean":
|
|
435
|
+
return { t: typeof input, v: input };
|
|
436
|
+
case "number":
|
|
437
|
+
if (Number.isNaN(input)) return { t: "number", v: "NaN" };
|
|
438
|
+
if (input === Infinity) return { t: "number", v: "Infinity" };
|
|
439
|
+
if (input === -Infinity) return { t: "number", v: "-Infinity" };
|
|
440
|
+
if (Object.is(input, -0)) return { t: "number", v: "-0" };
|
|
441
|
+
return { t: "number", v: input };
|
|
442
|
+
case "bigint":
|
|
443
|
+
return { t: "bigint", v: input.toString() };
|
|
444
|
+
case "symbol":
|
|
445
|
+
case "function":
|
|
446
|
+
throw new TypeError(`stableStringify: unsupported ${typeof input} value`);
|
|
447
|
+
}
|
|
448
|
+
const object = input;
|
|
449
|
+
if (ancestors.has(object)) throw new TypeError("stableStringify: cyclic value");
|
|
450
|
+
ancestors.add(object);
|
|
451
|
+
try {
|
|
452
|
+
if (input instanceof Date) {
|
|
453
|
+
return { t: "date", v: Number.isNaN(input.getTime()) ? "Invalid" : input.toISOString() };
|
|
454
|
+
}
|
|
455
|
+
if (input instanceof RegExp) return { t: "regexp", v: `re:${input.source}/${input.flags}` };
|
|
456
|
+
const bsonType = bsonTypeOf(object);
|
|
457
|
+
if (bsonType !== void 0) {
|
|
458
|
+
return { t: "bson", kind: bsonType, v: EJSON.stringify(input, { relaxed: false }) };
|
|
459
|
+
}
|
|
460
|
+
if (Array.isArray(input)) return { t: "array", v: input.map(visit) };
|
|
461
|
+
if (input instanceof Map) {
|
|
462
|
+
return {
|
|
463
|
+
t: "map",
|
|
464
|
+
v: [...input.entries()].map(([key, entry]) => [visit(key), visit(entry)])
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (input instanceof Set) return { t: "set", v: [...input].map(visit) };
|
|
468
|
+
if (Buffer.isBuffer(input)) return { t: "buffer", v: input.toString("base64") };
|
|
469
|
+
if (ArrayBuffer.isView(input)) {
|
|
470
|
+
return {
|
|
471
|
+
t: "bytes",
|
|
472
|
+
kind: input.constructor.name,
|
|
473
|
+
v: Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString("base64")
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
if (input instanceof ArrayBuffer) {
|
|
477
|
+
return { t: "arraybuffer", v: Buffer.from(input).toString("base64") };
|
|
478
|
+
}
|
|
479
|
+
if (isPlainObject(input)) {
|
|
480
|
+
const out = {};
|
|
481
|
+
for (const key of Object.keys(input).sort()) out[key] = visit(input[key]);
|
|
482
|
+
return { t: "object", v: out };
|
|
483
|
+
}
|
|
484
|
+
throw new TypeError(`stableStringify: unsupported object ${input.constructor.name}`);
|
|
485
|
+
} finally {
|
|
486
|
+
ancestors.delete(object);
|
|
368
487
|
}
|
|
369
|
-
return toPrimitive(v);
|
|
370
488
|
};
|
|
371
489
|
return JSON.stringify(visit(value));
|
|
372
490
|
};
|
|
373
|
-
var hashString = (input) =>
|
|
374
|
-
let h1 = 5381;
|
|
375
|
-
let h2 = 2166136261;
|
|
376
|
-
for (let i = 0; i < input.length; i++) {
|
|
377
|
-
const c = input.charCodeAt(i);
|
|
378
|
-
h1 = (h1 << 5) + h1 + c | 0;
|
|
379
|
-
h2 = (h2 ^ c) * 16777619 | 0;
|
|
380
|
-
h2 = h2 << 3 | h2 >>> 29;
|
|
381
|
-
}
|
|
382
|
-
return `${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
|
|
383
|
-
};
|
|
491
|
+
var hashString = (input) => createHash("sha256").update(input, "utf8").digest("base64url");
|
|
384
492
|
var stableHash = (value) => hashString(stableStringify(value));
|
|
385
493
|
|
|
386
494
|
// src/utils/lru.ts
|
|
@@ -618,16 +726,26 @@ var normalizeWatchId = (id) => {
|
|
|
618
726
|
};
|
|
619
727
|
|
|
620
728
|
// src/utils/watch-errors.ts
|
|
621
|
-
var
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
729
|
+
var STANDALONE = /only supported on replica sets|not supported on (?:a )?standalone/i;
|
|
730
|
+
var CREDENTIALS = /requires authentication|not authorized|unauthorized|authentication failed|Command .* requires authentication/i;
|
|
731
|
+
var explainPermanentWatchError = (message) => {
|
|
732
|
+
if (STANDALONE.test(message)) {
|
|
733
|
+
return {
|
|
734
|
+
reason: "standalone",
|
|
735
|
+
headline: "change streams need a replica set",
|
|
736
|
+
hint: "Point the connection at a replica set (a single-node replica set is fine for local dev), or set cacheWatch: false / cache: { ttlMs } to run write-through only."
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (CREDENTIALS.test(message)) {
|
|
740
|
+
return {
|
|
741
|
+
reason: "credentials",
|
|
742
|
+
headline: "the connection is not authorized for change streams",
|
|
743
|
+
hint: "Change streams need an authenticated user with the read + changeStream privilege (usually granted on the authSource database) \u2014 fix the credentials in .env, then restart."
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return void 0;
|
|
747
|
+
};
|
|
748
|
+
var isPermanentWatchError = (message) => explainPermanentWatchError(message) !== void 0;
|
|
631
749
|
|
|
632
750
|
// src/cache/hot-cache/watcher.ts
|
|
633
751
|
var resolveDbSafe = (ref, logger) => {
|
|
@@ -762,7 +880,7 @@ var WatchCoordinator = class {
|
|
|
762
880
|
}
|
|
763
881
|
}
|
|
764
882
|
if (isPermanentWatchError(message)) {
|
|
765
|
-
this.markStreamFailed(key);
|
|
883
|
+
this.markStreamFailed(key, explainPermanentWatchError(message));
|
|
766
884
|
return;
|
|
767
885
|
}
|
|
768
886
|
}
|
|
@@ -771,12 +889,15 @@ var WatchCoordinator = class {
|
|
|
771
889
|
await sleepJittered(backoff, 1e3, true);
|
|
772
890
|
}
|
|
773
891
|
}
|
|
774
|
-
markStreamFailed(key) {
|
|
892
|
+
markStreamFailed(key, verdict) {
|
|
775
893
|
this.failedStreams.add(key);
|
|
776
894
|
if (!this.host.isReplica()) return;
|
|
777
895
|
const planned = this.plannedStreamKeys();
|
|
778
896
|
if (planned.size > 0 && [...planned].every((k) => this.failedStreams.has(k))) {
|
|
779
|
-
this.host.logger.warn?.(
|
|
897
|
+
this.host.logger.warn?.(
|
|
898
|
+
verdict === void 0 ? {} : { reason: verdict.reason, hint: verdict.hint },
|
|
899
|
+
`hot cache: change streams unsupported${verdict === void 0 ? "" : ` \u2014 ${verdict.headline}`}, falling back to ticker`
|
|
900
|
+
);
|
|
780
901
|
this.host.fallbackToStandalone();
|
|
781
902
|
}
|
|
782
903
|
}
|
|
@@ -1194,7 +1315,7 @@ var createHotCache = (options) => new HotCache(options);
|
|
|
1194
1315
|
// src/cache/query-cache.ts
|
|
1195
1316
|
var SEP = "\0";
|
|
1196
1317
|
var NS_SEP = "";
|
|
1197
|
-
var cacheCollectionKey = (dbName, physical) => `${dbName}${NS_SEP}${physical}`;
|
|
1318
|
+
var cacheCollectionKey = (dbName, physical, clusterNamespace = "") => `${clusterNamespace}${NS_SEP}${dbName}${NS_SEP}${physical}`;
|
|
1198
1319
|
var QueryCache = class {
|
|
1199
1320
|
lru;
|
|
1200
1321
|
index = /* @__PURE__ */ new Map();
|
|
@@ -1219,8 +1340,8 @@ var QueryCache = class {
|
|
|
1219
1340
|
max: options.maxSize ?? 500,
|
|
1220
1341
|
onEvict: (key) => this.unregisterKey(key)
|
|
1221
1342
|
});
|
|
1222
|
-
this.ttlMs = options.ttlMs ??
|
|
1223
|
-
this.clone = options.clone ??
|
|
1343
|
+
this.ttlMs = options.ttlMs ?? 3e4;
|
|
1344
|
+
this.clone = options.clone ?? true;
|
|
1224
1345
|
}
|
|
1225
1346
|
get size() {
|
|
1226
1347
|
return this.lru.size;
|
|
@@ -1295,6 +1416,7 @@ var QueryCache = class {
|
|
|
1295
1416
|
this.sets++;
|
|
1296
1417
|
const ttl = ttlMs ?? this.ttlMs;
|
|
1297
1418
|
const expiresAt = ttl > 0 ? Date.now() + ttl : Infinity;
|
|
1419
|
+
this.unregisterKey(key);
|
|
1298
1420
|
this.lru.set(key, {
|
|
1299
1421
|
value,
|
|
1300
1422
|
expiresAt,
|
|
@@ -1353,14 +1475,24 @@ var withGracefulMongoTransaction = async (runner, fn, options = {}) => {
|
|
|
1353
1475
|
}
|
|
1354
1476
|
}
|
|
1355
1477
|
}
|
|
1356
|
-
|
|
1478
|
+
const unsupported = () => new InfraError(
|
|
1479
|
+
"TRANSACTIONS_UNSUPPORTED",
|
|
1480
|
+
"MongoDB deployment does not support transactions; set allowNonTransactionalFallback to run non-atomically"
|
|
1481
|
+
);
|
|
1482
|
+
if (!enabled) {
|
|
1483
|
+
if (options.allowNonTransactionalFallback === true) return fn(null);
|
|
1484
|
+
throw unsupported();
|
|
1485
|
+
}
|
|
1357
1486
|
try {
|
|
1358
|
-
return await runner.transaction(fn
|
|
1487
|
+
return await runner.transaction(fn, {
|
|
1488
|
+
...options.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: options.allowNonTransactionalFallback } : {}
|
|
1489
|
+
});
|
|
1359
1490
|
} catch (err) {
|
|
1360
1491
|
if (isTransactionUnsupportedError(err)) {
|
|
1492
|
+
if (options.allowNonTransactionalFallback !== true) throw err;
|
|
1361
1493
|
options.logger?.warn?.(
|
|
1362
1494
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
1363
|
-
"transactions unsupported
|
|
1495
|
+
"transactions unsupported; running explicitly requested non-transactional fallback"
|
|
1364
1496
|
);
|
|
1365
1497
|
return fn(null);
|
|
1366
1498
|
}
|
|
@@ -1527,89 +1659,134 @@ var nextMigrationNumber = async (dir) => {
|
|
|
1527
1659
|
return match ? Math.max(max, Number(match[1])) : max;
|
|
1528
1660
|
}, 0) + 1;
|
|
1529
1661
|
};
|
|
1530
|
-
|
|
1531
|
-
// src/migrations/journal.ts
|
|
1532
1662
|
var isDuplicateKeyError = (err) => {
|
|
1533
1663
|
const code = err?.code;
|
|
1534
1664
|
if (code === 11e3 || code === 11001) return true;
|
|
1535
1665
|
return /E11000 duplicate key/.test(String(err?.message ?? ""));
|
|
1536
1666
|
};
|
|
1667
|
+
var expiredLeaseFilter = (now, staleCutoff) => ({
|
|
1668
|
+
$or: [
|
|
1669
|
+
{ expiresAt: { $lt: now } },
|
|
1670
|
+
{ expiresAt: { $exists: false }, appliedAt: { $lt: staleCutoff } }
|
|
1671
|
+
]
|
|
1672
|
+
});
|
|
1537
1673
|
var createMigrationJournal = (manager) => {
|
|
1538
1674
|
let indexReady = false;
|
|
1539
1675
|
const ensureIndex = async () => {
|
|
1540
1676
|
if (indexReady) return;
|
|
1541
|
-
|
|
1542
|
-
await manager.createIndex
|
|
1543
|
-
} catch {
|
|
1677
|
+
if (manager.createIndex) {
|
|
1678
|
+
await manager.createIndex(JOURNAL_COLLECTION, { name: 1 }, { unique: true });
|
|
1544
1679
|
}
|
|
1545
1680
|
indexReady = true;
|
|
1546
1681
|
};
|
|
1547
1682
|
const appliedNames = async () => {
|
|
1548
|
-
const rows = await manager.findMany(JOURNAL_COLLECTION);
|
|
1683
|
+
const rows = await manager.findMany(JOURNAL_COLLECTION, {}, { cache: false });
|
|
1549
1684
|
return new Set(
|
|
1550
|
-
rows.filter(
|
|
1685
|
+
rows.filter(
|
|
1686
|
+
(row) => row.status === void 0 || row.status === "applied" || row.status === "rolling_back"
|
|
1687
|
+
).map((row) => row.name)
|
|
1551
1688
|
);
|
|
1552
1689
|
};
|
|
1553
|
-
const stealableFilter = (name, now, staleCutoff) => ({
|
|
1554
|
-
name,
|
|
1555
|
-
status: { $ne: "applied" },
|
|
1556
|
-
$or: [
|
|
1557
|
-
{ expiresAt: { $lt: now } },
|
|
1558
|
-
{ expiresAt: { $exists: false }, appliedAt: { $lt: staleCutoff } },
|
|
1559
|
-
{ status: { $nin: ["running", "applied"] } }
|
|
1560
|
-
]
|
|
1561
|
-
});
|
|
1562
1690
|
const claim = async (name, file, leaseMs) => {
|
|
1563
1691
|
await ensureIndex();
|
|
1564
1692
|
const now = /* @__PURE__ */ new Date();
|
|
1565
|
-
const
|
|
1693
|
+
const staleCutoff = new Date(Date.now() - leaseMs);
|
|
1694
|
+
const claimId = randomUUID();
|
|
1566
1695
|
const stole = await manager.findOneAndUpdate(
|
|
1567
1696
|
JOURNAL_COLLECTION,
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1697
|
+
{
|
|
1698
|
+
name,
|
|
1699
|
+
status: { $nin: ["applied", "rolling_back"] },
|
|
1700
|
+
...expiredLeaseFilter(now, staleCutoff)
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
$set: {
|
|
1704
|
+
status: "running",
|
|
1705
|
+
appliedAt: now,
|
|
1706
|
+
file,
|
|
1707
|
+
expiresAt: new Date(Date.now() + leaseMs),
|
|
1708
|
+
claimId
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
{ returnDocument: "before", cache: false }
|
|
1571
1712
|
);
|
|
1572
|
-
if (stole !== null) return
|
|
1713
|
+
if (stole !== null) return claimId;
|
|
1573
1714
|
try {
|
|
1574
1715
|
await manager.insertOne(JOURNAL_COLLECTION, {
|
|
1575
1716
|
name,
|
|
1576
1717
|
appliedAt: now,
|
|
1577
1718
|
status: "running",
|
|
1578
1719
|
file,
|
|
1579
|
-
expiresAt
|
|
1720
|
+
expiresAt: new Date(Date.now() + leaseMs),
|
|
1721
|
+
claimId
|
|
1580
1722
|
});
|
|
1581
|
-
return
|
|
1723
|
+
return claimId;
|
|
1582
1724
|
} catch (err) {
|
|
1583
|
-
if (isDuplicateKeyError(err)) return
|
|
1725
|
+
if (isDuplicateKeyError(err)) return null;
|
|
1584
1726
|
throw err;
|
|
1585
1727
|
}
|
|
1586
1728
|
};
|
|
1587
|
-
const
|
|
1729
|
+
const claimRollback = async (name, file, leaseMs) => {
|
|
1730
|
+
await ensureIndex();
|
|
1731
|
+
const now = /* @__PURE__ */ new Date();
|
|
1732
|
+
const claimId = randomUUID();
|
|
1733
|
+
const claimed = await manager.findOneAndUpdate(
|
|
1734
|
+
JOURNAL_COLLECTION,
|
|
1735
|
+
{
|
|
1736
|
+
name,
|
|
1737
|
+
$or: [
|
|
1738
|
+
{ status: "applied" },
|
|
1739
|
+
{ status: "rolling_back", ...expiredLeaseFilter(now, new Date(Date.now() - leaseMs)) }
|
|
1740
|
+
]
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
$set: {
|
|
1744
|
+
status: "rolling_back",
|
|
1745
|
+
file,
|
|
1746
|
+
claimId,
|
|
1747
|
+
expiresAt: new Date(Date.now() + leaseMs)
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
{ returnDocument: "before", cache: false }
|
|
1751
|
+
);
|
|
1752
|
+
return claimed === null ? null : claimId;
|
|
1753
|
+
};
|
|
1754
|
+
const renew = async (name, claimId, leaseMs) => {
|
|
1588
1755
|
await manager.updateOne(
|
|
1589
1756
|
JOURNAL_COLLECTION,
|
|
1590
|
-
{ name, status: "running" },
|
|
1591
|
-
{ $set: { expiresAt: new Date(Date.now() + leaseMs) } }
|
|
1757
|
+
{ name, claimId, status: { $in: ["running", "rolling_back"] } },
|
|
1758
|
+
{ $set: { expiresAt: new Date(Date.now() + leaseMs) } },
|
|
1759
|
+
{ cache: false }
|
|
1592
1760
|
);
|
|
1593
1761
|
};
|
|
1594
|
-
const markApplied = async (name) => {
|
|
1762
|
+
const markApplied = async (name, claimId) => {
|
|
1595
1763
|
const res = await manager.updateOne(
|
|
1596
1764
|
JOURNAL_COLLECTION,
|
|
1597
|
-
{ name, status: "running" },
|
|
1598
|
-
{ $set: { status: "applied" } }
|
|
1765
|
+
{ name, claimId, status: "running" },
|
|
1766
|
+
{ $set: { status: "applied" }, $unset: { expiresAt: "", claimId: "" } },
|
|
1767
|
+
{ cache: false }
|
|
1599
1768
|
);
|
|
1600
1769
|
if (res?.matchedCount === 0) {
|
|
1601
1770
|
await manager.insertOne(JOURNAL_COLLECTION, { name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }).catch(() => {
|
|
1602
1771
|
});
|
|
1772
|
+
throw new Error(`Migration "${name}" lost its claim before it could be marked applied`);
|
|
1603
1773
|
}
|
|
1604
1774
|
};
|
|
1605
|
-
const removeRunning = async (name) => {
|
|
1606
|
-
await manager.deleteOne(JOURNAL_COLLECTION, { name, status: "running" }).catch(() => {
|
|
1775
|
+
const removeRunning = async (name, claimId) => {
|
|
1776
|
+
await manager.deleteOne(JOURNAL_COLLECTION, { name, claimId, status: "running" }, { cache: false }).catch(() => {
|
|
1607
1777
|
});
|
|
1608
1778
|
};
|
|
1609
|
-
const unrecord = async (name) => {
|
|
1610
|
-
await manager.deleteOne(
|
|
1779
|
+
const unrecord = async (name, claimId) => {
|
|
1780
|
+
const result = await manager.deleteOne(
|
|
1781
|
+
JOURNAL_COLLECTION,
|
|
1782
|
+
{ name, claimId, status: "rolling_back" },
|
|
1783
|
+
{ cache: false }
|
|
1784
|
+
);
|
|
1785
|
+
if (result?.deletedCount === 0) {
|
|
1786
|
+
throw new Error(`Migration "${name}" lost its rollback claim`);
|
|
1787
|
+
}
|
|
1611
1788
|
};
|
|
1612
|
-
return { appliedNames, claim, renew, markApplied, removeRunning, unrecord };
|
|
1789
|
+
return { appliedNames, claim, claimRollback, renew, markApplied, removeRunning, unrecord };
|
|
1613
1790
|
};
|
|
1614
1791
|
|
|
1615
1792
|
// src/migrations/index.ts
|
|
@@ -1629,10 +1806,10 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1629
1806
|
}
|
|
1630
1807
|
return db;
|
|
1631
1808
|
};
|
|
1632
|
-
const startHeartbeat = (journal, name) => {
|
|
1809
|
+
const startHeartbeat = (journal, name, claimId) => {
|
|
1633
1810
|
const timer = setInterval(
|
|
1634
1811
|
() => {
|
|
1635
|
-
void journal.renew(name, leaseMs).catch(() => {
|
|
1812
|
+
void journal.renew(name, claimId, leaseMs).catch(() => {
|
|
1636
1813
|
});
|
|
1637
1814
|
},
|
|
1638
1815
|
Math.max(leaseMs / 2, 1e3)
|
|
@@ -1649,19 +1826,20 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1649
1826
|
for (const file of pending) {
|
|
1650
1827
|
const migration = await loadMigration(migrationDir, file);
|
|
1651
1828
|
logger.info({ name: migration.name }, "running migration up");
|
|
1652
|
-
|
|
1829
|
+
const claimId = await journal.claim(migration.name, basename(file), leaseMs);
|
|
1830
|
+
if (claimId === null) {
|
|
1653
1831
|
logger.info(
|
|
1654
1832
|
{ name: migration.name },
|
|
1655
1833
|
"migration claimed by another runner or already applied, skipping"
|
|
1656
1834
|
);
|
|
1657
1835
|
continue;
|
|
1658
1836
|
}
|
|
1659
|
-
const stopHeartbeat = startHeartbeat(journal, migration.name);
|
|
1837
|
+
const stopHeartbeat = startHeartbeat(journal, migration.name, claimId);
|
|
1660
1838
|
try {
|
|
1661
1839
|
await migration.up({ service, logger, name: migration.name });
|
|
1662
|
-
await journal.markApplied(migration.name);
|
|
1840
|
+
await journal.markApplied(migration.name, claimId);
|
|
1663
1841
|
} catch (err) {
|
|
1664
|
-
await journal.removeRunning(migration.name);
|
|
1842
|
+
await journal.removeRunning(migration.name, claimId);
|
|
1665
1843
|
throw err;
|
|
1666
1844
|
} finally {
|
|
1667
1845
|
stopHeartbeat();
|
|
@@ -1684,9 +1862,19 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1684
1862
|
const appliedFiles = files.filter((file) => applied.has(migrationNameOf(file))).reverse();
|
|
1685
1863
|
for (const file of appliedFiles) {
|
|
1686
1864
|
const migration = await loadMigration(migrationDir, file);
|
|
1865
|
+
const claimId = await journal.claimRollback(migration.name, basename(file), leaseMs);
|
|
1866
|
+
if (claimId === null) {
|
|
1867
|
+
logger.info({ name: migration.name }, "migration rollback already claimed, skipping");
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const stopHeartbeat = startHeartbeat(journal, migration.name, claimId);
|
|
1687
1871
|
logger.info({ name: migration.name }, "running migration down");
|
|
1688
|
-
|
|
1689
|
-
|
|
1872
|
+
try {
|
|
1873
|
+
await migration.down({ service, logger, name: migration.name });
|
|
1874
|
+
await journal.unrecord(migration.name, claimId);
|
|
1875
|
+
} finally {
|
|
1876
|
+
stopHeartbeat();
|
|
1877
|
+
}
|
|
1690
1878
|
logger.info({ name: migration.name }, "migration rolled back");
|
|
1691
1879
|
if (targetName !== void 0 && migration.name === targetName) break;
|
|
1692
1880
|
}
|
|
@@ -2227,6 +2415,16 @@ var toMongoValidator = (schema) => ({
|
|
|
2227
2415
|
});
|
|
2228
2416
|
|
|
2229
2417
|
// src/schema/types.ts
|
|
2418
|
+
var SUPPORTED_STRING_FORMATS = /* @__PURE__ */ new Set([
|
|
2419
|
+
"email",
|
|
2420
|
+
"uri",
|
|
2421
|
+
"date-time",
|
|
2422
|
+
"date",
|
|
2423
|
+
"uuid",
|
|
2424
|
+
"hostname",
|
|
2425
|
+
"ipv4",
|
|
2426
|
+
"ipv6"
|
|
2427
|
+
]);
|
|
2230
2428
|
var chain = (field) => {
|
|
2231
2429
|
const obj = field;
|
|
2232
2430
|
obj.optional = () => chain({ ...field, flags: { ...field.flags, optional: true } });
|
|
@@ -2240,6 +2438,11 @@ function objectField(properties, opts = {}) {
|
|
|
2240
2438
|
var s = {
|
|
2241
2439
|
/** UTF-8 string field (optional `format` / length / regex constraints). */
|
|
2242
2440
|
string(opts = {}) {
|
|
2441
|
+
if (opts.format !== void 0 && !SUPPORTED_STRING_FORMATS.has(opts.format)) {
|
|
2442
|
+
throw new Error(
|
|
2443
|
+
`Unsupported string format "${opts.format}"; supported: ${[...SUPPORTED_STRING_FORMATS].join(", ")}`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2243
2446
|
return chain({ kind: "string", ...BASE, ...opts });
|
|
2244
2447
|
},
|
|
2245
2448
|
/** 32-bit integer field with optional bounds (min/max, exclusive, multipleOf). */
|
|
@@ -2405,6 +2608,200 @@ var checkReservedValue = (key, value, path, issues) => {
|
|
|
2405
2608
|
`Reserved field "${key}" must be ${types.join(" | ")}`
|
|
2406
2609
|
);
|
|
2407
2610
|
};
|
|
2611
|
+
var typeMatches = (value, type) => {
|
|
2612
|
+
if (type === "number") return typeof value === "number" || value instanceof BSON.Long;
|
|
2613
|
+
if (type === "int") {
|
|
2614
|
+
return value instanceof BSON.Int32 || typeof value === "number" && Number.isInteger(value);
|
|
2615
|
+
}
|
|
2616
|
+
if (type === "long") {
|
|
2617
|
+
return value instanceof BSON.Long || typeof value === "number" && Number.isInteger(value);
|
|
2618
|
+
}
|
|
2619
|
+
if (type === "double") return typeof value === "number" || value instanceof BSON.Double;
|
|
2620
|
+
if (type === "decimal") return value instanceof BSON.Decimal128;
|
|
2621
|
+
if (type === "object")
|
|
2622
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2623
|
+
if (type === "array") return Array.isArray(value);
|
|
2624
|
+
if (type === "string") return typeof value === "string";
|
|
2625
|
+
if (type === "bool" || type === "boolean") return typeof value === "boolean";
|
|
2626
|
+
if (type === "date") return value instanceof Date;
|
|
2627
|
+
if (type === "objectId") return value instanceof BSON.ObjectId;
|
|
2628
|
+
if (type === "null") return value === null;
|
|
2629
|
+
if (type === "binData") return value instanceof BSON.Binary;
|
|
2630
|
+
if (type === "regex") return value instanceof RegExp;
|
|
2631
|
+
return true;
|
|
2632
|
+
};
|
|
2633
|
+
var comparable = (value) => {
|
|
2634
|
+
if (typeof value === "number") return value;
|
|
2635
|
+
if (value instanceof BSON.Long || value instanceof BSON.Int32 || value instanceof BSON.Double) {
|
|
2636
|
+
return Number(value.toString());
|
|
2637
|
+
}
|
|
2638
|
+
if (value instanceof BSON.Decimal128) return Number(value.toString());
|
|
2639
|
+
return void 0;
|
|
2640
|
+
};
|
|
2641
|
+
var validateRawSchema = (schema, value, path, issues) => {
|
|
2642
|
+
const types = schema.bsonType === void 0 ? [] : [schema.bsonType].flat();
|
|
2643
|
+
if (types.length > 0 && !types.some((type) => typeMatches(value, type))) {
|
|
2644
|
+
push(issues, path, "type", types.join(" | "), "BSON type does not match raw schema");
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
if (schema.enum && !schema.enum.some(
|
|
2648
|
+
(candidate) => BSON.EJSON.stringify(candidate) === BSON.EJSON.stringify(value)
|
|
2649
|
+
)) {
|
|
2650
|
+
push(issues, path, "enum", "raw enum", "Value is not in the allowed set");
|
|
2651
|
+
}
|
|
2652
|
+
if (typeof value === "string") {
|
|
2653
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
2654
|
+
push(issues, path, "constraint", `minLength ${schema.minLength}`, "String is too short");
|
|
2655
|
+
}
|
|
2656
|
+
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
2657
|
+
push(issues, path, "constraint", `maxLength ${schema.maxLength}`, "String is too long");
|
|
2658
|
+
}
|
|
2659
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
2660
|
+
push(
|
|
2661
|
+
issues,
|
|
2662
|
+
path,
|
|
2663
|
+
"constraint",
|
|
2664
|
+
`pattern ${schema.pattern}`,
|
|
2665
|
+
"String does not match pattern"
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
const numeric = comparable(value);
|
|
2670
|
+
if (numeric !== void 0) {
|
|
2671
|
+
const minimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.minimum;
|
|
2672
|
+
const maximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.maximum;
|
|
2673
|
+
if (minimum !== void 0 && (numeric < minimum || schema.exclusiveMinimum === true && numeric === minimum)) {
|
|
2674
|
+
push(issues, path, "constraint", `minimum ${minimum}`, "Number is below minimum");
|
|
2675
|
+
}
|
|
2676
|
+
if (maximum !== void 0 && (numeric > maximum || schema.exclusiveMaximum === true && numeric === maximum)) {
|
|
2677
|
+
push(issues, path, "constraint", `maximum ${maximum}`, "Number is above maximum");
|
|
2678
|
+
}
|
|
2679
|
+
if (schema.multipleOf !== void 0 && Math.abs(numeric / schema.multipleOf - Math.round(numeric / schema.multipleOf)) > 1e-9) {
|
|
2680
|
+
push(
|
|
2681
|
+
issues,
|
|
2682
|
+
path,
|
|
2683
|
+
"constraint",
|
|
2684
|
+
`multipleOf ${schema.multipleOf}`,
|
|
2685
|
+
"Number is not a multiple"
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (Array.isArray(value)) {
|
|
2690
|
+
if (schema.minItems !== void 0 && value.length < schema.minItems) {
|
|
2691
|
+
push(issues, path, "constraint", `minItems ${schema.minItems}`, "Array is too short");
|
|
2692
|
+
}
|
|
2693
|
+
if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
|
|
2694
|
+
push(issues, path, "constraint", `maxItems ${schema.maxItems}`, "Array is too long");
|
|
2695
|
+
}
|
|
2696
|
+
if (schema.uniqueItems) {
|
|
2697
|
+
const unique = new Set(value.map((item) => BSON.EJSON.stringify(item, { relaxed: false })));
|
|
2698
|
+
if (unique.size !== value.length) {
|
|
2699
|
+
push(issues, path, "constraint", "uniqueItems", "Array contains duplicate items");
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (Array.isArray(schema.items)) {
|
|
2703
|
+
schema.items.forEach((item, index) => {
|
|
2704
|
+
if (index < value.length)
|
|
2705
|
+
validateRawSchema(item, value[index], `${path}[${index}]`, issues);
|
|
2706
|
+
});
|
|
2707
|
+
} else if (schema.items) {
|
|
2708
|
+
value.forEach((item, index) => {
|
|
2709
|
+
validateRawSchema(schema.items, item, `${path}[${index}]`, issues);
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
2714
|
+
const object = value;
|
|
2715
|
+
for (const required of schema.required ?? []) {
|
|
2716
|
+
if (object[required] === void 0)
|
|
2717
|
+
push(issues, `${path}.${required}`, "missing", "required", "Required field is missing");
|
|
2718
|
+
}
|
|
2719
|
+
for (const [key, child] of Object.entries(schema.properties ?? {})) {
|
|
2720
|
+
if (key in object)
|
|
2721
|
+
validateRawSchema(child, object[key], path ? `${path}.${key}` : key, issues);
|
|
2722
|
+
}
|
|
2723
|
+
for (const [pattern, child] of Object.entries(schema.patternProperties ?? {})) {
|
|
2724
|
+
for (const key of Object.keys(object)) {
|
|
2725
|
+
if (new RegExp(pattern).test(key))
|
|
2726
|
+
validateRawSchema(child, object[key], `${path}.${key}`, issues);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (schema.additionalProperties === false) {
|
|
2730
|
+
const known = /* @__PURE__ */ new Set([
|
|
2731
|
+
...Object.keys(schema.properties ?? {}),
|
|
2732
|
+
...Object.keys(object).filter(
|
|
2733
|
+
(key) => Object.keys(schema.patternProperties ?? {}).some(
|
|
2734
|
+
(pattern) => new RegExp(pattern).test(key)
|
|
2735
|
+
)
|
|
2736
|
+
)
|
|
2737
|
+
]);
|
|
2738
|
+
for (const key of Object.keys(object)) {
|
|
2739
|
+
if (!known.has(key))
|
|
2740
|
+
push(
|
|
2741
|
+
issues,
|
|
2742
|
+
`${path}.${key}`,
|
|
2743
|
+
"unknown_key",
|
|
2744
|
+
"no additional properties",
|
|
2745
|
+
"Unexpected field"
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
} else if (typeof schema.additionalProperties === "object") {
|
|
2749
|
+
for (const key of Object.keys(object)) {
|
|
2750
|
+
if (!(key in (schema.properties ?? {})))
|
|
2751
|
+
validateRawSchema(schema.additionalProperties, object[key], `${path}.${key}`, issues);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
const count = Object.keys(object).length;
|
|
2755
|
+
if (schema.minProperties !== void 0 && count < schema.minProperties)
|
|
2756
|
+
push(
|
|
2757
|
+
issues,
|
|
2758
|
+
path,
|
|
2759
|
+
"constraint",
|
|
2760
|
+
`minProperties ${schema.minProperties}`,
|
|
2761
|
+
"Too few properties"
|
|
2762
|
+
);
|
|
2763
|
+
if (schema.maxProperties !== void 0 && count > schema.maxProperties)
|
|
2764
|
+
push(
|
|
2765
|
+
issues,
|
|
2766
|
+
path,
|
|
2767
|
+
"constraint",
|
|
2768
|
+
`maxProperties ${schema.maxProperties}`,
|
|
2769
|
+
"Too many properties"
|
|
2770
|
+
);
|
|
2771
|
+
for (const [key, dependency] of Object.entries(schema.dependencies ?? {})) {
|
|
2772
|
+
if (!(key in object)) continue;
|
|
2773
|
+
if (Array.isArray(dependency)) {
|
|
2774
|
+
for (const required of dependency)
|
|
2775
|
+
if (!(required in object))
|
|
2776
|
+
push(issues, path, "constraint", "dependencies", `${key} requires ${required}`);
|
|
2777
|
+
} else validateRawSchema(dependency, object, path, issues);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
for (const child of schema.allOf ?? []) validateRawSchema(child, value, path, issues);
|
|
2781
|
+
if (schema.anyOf) {
|
|
2782
|
+
const matches = schema.anyOf.some((child) => {
|
|
2783
|
+
const candidate = [];
|
|
2784
|
+
validateRawSchema(child, value, path, candidate);
|
|
2785
|
+
return candidate.length === 0;
|
|
2786
|
+
});
|
|
2787
|
+
if (!matches) push(issues, path, "constraint", "anyOf", "No anyOf schema matched");
|
|
2788
|
+
}
|
|
2789
|
+
if (schema.oneOf) {
|
|
2790
|
+
const matches = schema.oneOf.filter((child) => {
|
|
2791
|
+
const candidate = [];
|
|
2792
|
+
validateRawSchema(child, value, path, candidate);
|
|
2793
|
+
return candidate.length === 0;
|
|
2794
|
+
}).length;
|
|
2795
|
+
if (matches !== 1)
|
|
2796
|
+
push(issues, path, "constraint", "oneOf", "Exactly one oneOf schema must match");
|
|
2797
|
+
}
|
|
2798
|
+
if (schema.not) {
|
|
2799
|
+
const candidate = [];
|
|
2800
|
+
validateRawSchema(schema.not, value, path, candidate);
|
|
2801
|
+
if (candidate.length === 0)
|
|
2802
|
+
push(issues, path, "constraint", "not", "Value matched a forbidden schema");
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2408
2805
|
|
|
2409
2806
|
// src/schema/validate-doc/field-validators.ts
|
|
2410
2807
|
var patternCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -2420,6 +2817,22 @@ var compiledPattern = (field) => {
|
|
|
2420
2817
|
return void 0;
|
|
2421
2818
|
}
|
|
2422
2819
|
};
|
|
2820
|
+
var STRING_FORMAT_VALIDATORS = {
|
|
2821
|
+
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
2822
|
+
uri: (value) => {
|
|
2823
|
+
try {
|
|
2824
|
+
return new URL(value).protocol.length > 0;
|
|
2825
|
+
} catch {
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
},
|
|
2829
|
+
"date-time": (value) => /^\d{4}-\d{2}-\d{2}T/.test(value) && !Number.isNaN(Date.parse(value)),
|
|
2830
|
+
date: (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)),
|
|
2831
|
+
uuid: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value),
|
|
2832
|
+
hostname: (value) => /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i.test(value),
|
|
2833
|
+
ipv4: (value) => isIP(value) === 4,
|
|
2834
|
+
ipv6: (value) => isIP(value) === 6
|
|
2835
|
+
};
|
|
2423
2836
|
var stringValidator = (_deps, field, value, path, issues) => {
|
|
2424
2837
|
const f = field;
|
|
2425
2838
|
if (typeof value !== "string") {
|
|
@@ -2454,6 +2867,16 @@ var stringValidator = (_deps, field, value, path, issues) => {
|
|
|
2454
2867
|
`"${value}" does not match pattern`
|
|
2455
2868
|
);
|
|
2456
2869
|
}
|
|
2870
|
+
const format = f.format ? STRING_FORMAT_VALIDATORS[f.format] : void 0;
|
|
2871
|
+
if (format && !format(value)) {
|
|
2872
|
+
push(
|
|
2873
|
+
issues,
|
|
2874
|
+
path,
|
|
2875
|
+
"constraint",
|
|
2876
|
+
`string(format ${f.format})`,
|
|
2877
|
+
`"${value}" is not a valid ${f.format}`
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2457
2880
|
};
|
|
2458
2881
|
var numberValidator = (_deps, field, value, path, issues) => {
|
|
2459
2882
|
const f = field;
|
|
@@ -2524,7 +2947,8 @@ var nullValidator = (_deps, _field, value, path, issues) => {
|
|
|
2524
2947
|
};
|
|
2525
2948
|
var anyValidator = () => {
|
|
2526
2949
|
};
|
|
2527
|
-
var rawValidator = () => {
|
|
2950
|
+
var rawValidator = (_deps, field, value, path, issues) => {
|
|
2951
|
+
validateRawSchema(field.fragment, value, path, issues);
|
|
2528
2952
|
};
|
|
2529
2953
|
var enumValidator = (_deps, field, value, path, issues) => {
|
|
2530
2954
|
const f = field;
|
|
@@ -2794,13 +3218,13 @@ var CacheInvalidator = class {
|
|
|
2794
3218
|
connectedAt = Date.now();
|
|
2795
3219
|
if (backoff > 0) {
|
|
2796
3220
|
this.cache.invalidateByCollection(
|
|
2797
|
-
cacheCollectionKey(ref.db.databaseName, ref.collection)
|
|
3221
|
+
cacheCollectionKey(ref.db.databaseName, ref.collection, ref.namespace)
|
|
2798
3222
|
);
|
|
2799
3223
|
}
|
|
2800
3224
|
const iterator = stream[Symbol.asyncIterator]();
|
|
2801
3225
|
for await (const _change of iterator) {
|
|
2802
3226
|
this.cache.invalidateByCollection(
|
|
2803
|
-
cacheCollectionKey(ref.db.databaseName, ref.collection)
|
|
3227
|
+
cacheCollectionKey(ref.db.databaseName, ref.collection, ref.namespace)
|
|
2804
3228
|
);
|
|
2805
3229
|
}
|
|
2806
3230
|
} catch (err) {
|
|
@@ -2814,13 +3238,19 @@ var CacheInvalidator = class {
|
|
|
2814
3238
|
} catch {
|
|
2815
3239
|
}
|
|
2816
3240
|
}
|
|
2817
|
-
|
|
3241
|
+
const verdict = explainPermanentWatchError(message);
|
|
3242
|
+
if (verdict) {
|
|
2818
3243
|
this.disabled = true;
|
|
2819
3244
|
if (!this.warnedUnsupported) {
|
|
2820
3245
|
this.warnedUnsupported = true;
|
|
2821
3246
|
this.logger.warn?.(
|
|
2822
|
-
{
|
|
2823
|
-
|
|
3247
|
+
{
|
|
3248
|
+
collection: ref.collection,
|
|
3249
|
+
reason: verdict.reason,
|
|
3250
|
+
error: message,
|
|
3251
|
+
hint: verdict.hint
|
|
3252
|
+
},
|
|
3253
|
+
`cacheWatch: disabled \u2014 ${verdict.headline}; the query cache stays write-through only`
|
|
2824
3254
|
);
|
|
2825
3255
|
}
|
|
2826
3256
|
return;
|
|
@@ -2911,7 +3341,8 @@ var makeGetDbUrl = (dbClients, config) => (name) => {
|
|
|
2911
3341
|
};
|
|
2912
3342
|
var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClients = /* @__PURE__ */ new Map()) => {
|
|
2913
3343
|
const connecting = /* @__PURE__ */ new Map();
|
|
2914
|
-
|
|
3344
|
+
let transition = Promise.resolve();
|
|
3345
|
+
const connect = async (names) => {
|
|
2915
3346
|
const keys = names && names.length > 0 ? names : Object.keys(dbClients);
|
|
2916
3347
|
const openedThisCall = [];
|
|
2917
3348
|
const assignedThisCall = [];
|
|
@@ -2929,6 +3360,7 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
|
|
|
2929
3360
|
const c = new MongoClient(dbUrl, {
|
|
2930
3361
|
connectTimeoutMS: definition.connectTimeoutMs ?? 1e4,
|
|
2931
3362
|
serverSelectionTimeoutMS: definition.connectTimeoutMs ?? 1e4,
|
|
3363
|
+
monitorCommands: definition.monitorCommands ?? false,
|
|
2932
3364
|
...definition.readPreference && { readPreference: definition.readPreference },
|
|
2933
3365
|
...definition.readConcern && { readConcern: definition.readConcern },
|
|
2934
3366
|
...definition.writeConcern && { writeConcern: definition.writeConcern }
|
|
@@ -2959,18 +3391,20 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
|
|
|
2959
3391
|
throw err;
|
|
2960
3392
|
}
|
|
2961
3393
|
};
|
|
3394
|
+
return (names) => {
|
|
3395
|
+
const result = transition.then(() => connect(names));
|
|
3396
|
+
transition = result.catch(() => {
|
|
3397
|
+
});
|
|
3398
|
+
return result;
|
|
3399
|
+
};
|
|
2962
3400
|
};
|
|
2963
3401
|
var closeConnections = async (openedClients) => {
|
|
2964
|
-
const
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
});
|
|
2968
|
-
} catch {
|
|
2969
|
-
return Promise.resolve();
|
|
2970
|
-
}
|
|
2971
|
-
});
|
|
2972
|
-
await Promise.allSettled(closes);
|
|
3402
|
+
const results = await Promise.allSettled(
|
|
3403
|
+
[...openedClients.values()].map((client) => client.close())
|
|
3404
|
+
);
|
|
2973
3405
|
openedClients.clear();
|
|
3406
|
+
const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
3407
|
+
if (errors.length > 0) throw new AggregateError(errors, "Failed to close MongoDB connections");
|
|
2974
3408
|
};
|
|
2975
3409
|
|
|
2976
3410
|
// src/service/health.ts
|
|
@@ -3012,9 +3446,19 @@ var createForeignKeyLoader = (deps2, collection, foreignField, options = {}) =>
|
|
|
3012
3446
|
collection,
|
|
3013
3447
|
{ [foreignField]: { $in: keys } },
|
|
3014
3448
|
{
|
|
3015
|
-
|
|
3449
|
+
limit: options.maxDocs !== void 0 ? options.maxDocs + 1 : 0,
|
|
3450
|
+
...options.session !== void 0 ? { session: options.session } : {},
|
|
3451
|
+
...options.maxTimeMS !== void 0 ? { maxTimeMS: options.maxTimeMS } : {},
|
|
3452
|
+
...options.readPreference !== void 0 ? { readPreference: options.readPreference } : {},
|
|
3453
|
+
...options.readConcern !== void 0 ? { readConcern: options.readConcern } : {},
|
|
3454
|
+
...options.queryCache !== void 0 ? { queryCache: options.queryCache } : {}
|
|
3016
3455
|
}
|
|
3017
3456
|
);
|
|
3457
|
+
if (options.maxDocs !== void 0 && docs.length > options.maxDocs) {
|
|
3458
|
+
throw new BadRequest(
|
|
3459
|
+
`populate: ${collection}.${foreignField} exceeded maxDocs ${options.maxDocs}; raise the cap or narrow the relation`
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3018
3462
|
const index = /* @__PURE__ */ new Map();
|
|
3019
3463
|
for (const doc of docs) {
|
|
3020
3464
|
const value = doc[foreignField];
|
|
@@ -3177,8 +3621,11 @@ var traceDbOp = async (logger, meta, fn, options = {}) => {
|
|
|
3177
3621
|
};
|
|
3178
3622
|
|
|
3179
3623
|
// src/service/op-deps.ts
|
|
3180
|
-
var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors) => {
|
|
3181
|
-
const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, {
|
|
3624
|
+
var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors, debugQuery) => {
|
|
3625
|
+
const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, {
|
|
3626
|
+
wrapMongoErrors,
|
|
3627
|
+
...debugQuery !== void 0 ? { debugQuery } : {}
|
|
3628
|
+
});
|
|
3182
3629
|
const meta = (collection, op, params) => ({
|
|
3183
3630
|
collection: String(collection),
|
|
3184
3631
|
physicalCollection: resolve(String(collection)),
|
|
@@ -3580,20 +4027,20 @@ var createCachedAggregate = (ctx) => {
|
|
|
3580
4027
|
const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
|
|
3581
4028
|
const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options, false, { pipeline });
|
|
3582
4029
|
const key = useCache ? cache.key(
|
|
3583
|
-
cacheCollectionKey(client.databaseName, physical),
|
|
4030
|
+
cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace),
|
|
3584
4031
|
stableHash([opName, pipeline, options])
|
|
3585
4032
|
) : void 0;
|
|
3586
4033
|
const toSourceKeys = () => collectAggSources(String(collection), pipeline, sources).map((name) => {
|
|
3587
4034
|
try {
|
|
3588
|
-
return cacheCollectionKey(client.databaseName, resolve(name));
|
|
4035
|
+
return cacheCollectionKey(client.databaseName, resolve(name), opts.cacheNamespace);
|
|
3589
4036
|
} catch {
|
|
3590
|
-
return cacheCollectionKey(client.databaseName, name);
|
|
4037
|
+
return cacheCollectionKey(client.databaseName, name, opts.cacheNamespace);
|
|
3591
4038
|
}
|
|
3592
4039
|
});
|
|
3593
4040
|
const runOnce = () => {
|
|
3594
4041
|
if (useCache && key) {
|
|
3595
4042
|
const sourceKeys = toSourceKeys();
|
|
3596
|
-
const primaryKey = cacheCollectionKey(client.databaseName, physical);
|
|
4043
|
+
const primaryKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
|
|
3597
4044
|
const versions = { [primaryKey]: cache.versionOf(primaryKey) };
|
|
3598
4045
|
for (const col of sourceKeys) {
|
|
3599
4046
|
if (!(col in versions)) versions[col] = cache.versionOf(col);
|
|
@@ -3608,8 +4055,8 @@ var createCachedAggregate = (ctx) => {
|
|
|
3608
4055
|
return runDriver();
|
|
3609
4056
|
};
|
|
3610
4057
|
if (shouldDedupe) {
|
|
3611
|
-
const primaryKey = cacheCollectionKey(client.databaseName, physical);
|
|
3612
|
-
const gen = cache !== void 0 ? cache.versionOf(
|
|
4058
|
+
const primaryKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
|
|
4059
|
+
const gen = cache !== void 0 ? toSourceKeys().map((source) => `${source}:${cache.versionOf(source)}`).join("|") : "";
|
|
3613
4060
|
const dedupeKey = `${primaryKey}|${gen}|${opName}|${stableHash([pipeline, options])}`;
|
|
3614
4061
|
return await opts.inFlight.run(dedupeKey, runOnce);
|
|
3615
4062
|
}
|
|
@@ -3818,7 +4265,13 @@ var makeTextSearchOp = (ctx) => {
|
|
|
3818
4265
|
// src/service/aggregation/index.ts
|
|
3819
4266
|
var makeAggregationOps = (client, dbLabel, logger, opts) => {
|
|
3820
4267
|
const resolve = opts.resolveCollectionName;
|
|
3821
|
-
const deps2 = makeOpDeps(
|
|
4268
|
+
const deps2 = makeOpDeps(
|
|
4269
|
+
logger,
|
|
4270
|
+
dbLabel,
|
|
4271
|
+
resolve,
|
|
4272
|
+
opts.wrapMongoErrors === true,
|
|
4273
|
+
opts.debugQuery
|
|
4274
|
+
);
|
|
3822
4275
|
const coll = (collection) => client.collection(resolve(String(collection)));
|
|
3823
4276
|
const ctx = {
|
|
3824
4277
|
resolve,
|
|
@@ -3836,11 +4289,7 @@ var makeAggregationOps = (client, dbLabel, logger, opts) => {
|
|
|
3836
4289
|
};
|
|
3837
4290
|
|
|
3838
4291
|
// src/schema/apply-defaults.ts
|
|
3839
|
-
var cloneDefault = (value) =>
|
|
3840
|
-
if (value === null || typeof value !== "object") return value;
|
|
3841
|
-
if (typeof structuredClone === "function") return structuredClone(value);
|
|
3842
|
-
return Array.isArray(value) ? [...value] : { ...value };
|
|
3843
|
-
};
|
|
4292
|
+
var cloneDefault = (value) => cloneDeep(value);
|
|
3844
4293
|
var applySchemaDefaults = (schema, doc) => {
|
|
3845
4294
|
if (!schema || doc === null || typeof doc !== "object" || Array.isArray(doc)) return;
|
|
3846
4295
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
@@ -3889,7 +4338,9 @@ var checkDocsDrift = (options, collection, op, docs) => {
|
|
|
3889
4338
|
const schema = options.getSchema?.(collection);
|
|
3890
4339
|
if (!schema) return;
|
|
3891
4340
|
for (const doc of docs) {
|
|
3892
|
-
const issues = validateDoc(schema, doc)
|
|
4341
|
+
const issues = validateDoc(schema, doc).filter(
|
|
4342
|
+
(issue) => options.partial !== true || issue.code !== "missing"
|
|
4343
|
+
);
|
|
3893
4344
|
if (issues.length === 0) continue;
|
|
3894
4345
|
const detail = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
3895
4346
|
const extra = { collection, db: options.db, op, issues };
|
|
@@ -3923,7 +4374,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3923
4374
|
logger,
|
|
3924
4375
|
dbLabel,
|
|
3925
4376
|
resolve,
|
|
3926
|
-
opts.wrapMongoErrors === true
|
|
4377
|
+
opts.wrapMongoErrors === true,
|
|
4378
|
+
opts.debugQuery
|
|
3927
4379
|
);
|
|
3928
4380
|
const coll = (name) => client.collection(resolve(String(name)));
|
|
3929
4381
|
const toDriverFilter = (filter) => filter;
|
|
@@ -3931,7 +4383,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3931
4383
|
const writeRun = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, true, params);
|
|
3932
4384
|
const invalidate = (collection) => {
|
|
3933
4385
|
opts.cache?.invalidateByCollection(
|
|
3934
|
-
cacheCollectionKey(client.databaseName, resolve(String(collection)))
|
|
4386
|
+
cacheCollectionKey(client.databaseName, resolve(String(collection)), opts.cacheNamespace)
|
|
3935
4387
|
);
|
|
3936
4388
|
};
|
|
3937
4389
|
const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
|
|
@@ -3961,6 +4413,26 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3961
4413
|
if (!ts?.updatedAt) return doc;
|
|
3962
4414
|
return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
|
|
3963
4415
|
};
|
|
4416
|
+
const validateWrite = (collection, doc, generateId = true) => {
|
|
4417
|
+
if (opts.validateWrites === false) return;
|
|
4418
|
+
const schema = opts.getSchema?.(String(collection));
|
|
4419
|
+
if (!schema) return;
|
|
4420
|
+
if (generateId && doc._id === void 0 && schema.properties._id?.kind === "objectId") {
|
|
4421
|
+
doc._id = new ObjectId();
|
|
4422
|
+
}
|
|
4423
|
+
const issues = validateDoc(schema, doc).filter(
|
|
4424
|
+
(issue) => generateId || issue.path !== "_id" || issue.code !== "missing"
|
|
4425
|
+
);
|
|
4426
|
+
if (issues.length > 0) {
|
|
4427
|
+
throw new DomainError("VALIDATION_FAILED", "Document failed schema validation", {
|
|
4428
|
+
db: client.databaseName,
|
|
4429
|
+
collection: String(collection),
|
|
4430
|
+
...doc._id !== void 0 ? { documentId: doc._id } : {},
|
|
4431
|
+
fields: [...new Set(issues.map((issue) => issue.path))],
|
|
4432
|
+
issues
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
};
|
|
3964
4436
|
const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
|
|
3965
4437
|
const physical = resolve(String(collection));
|
|
3966
4438
|
const resolved = resolveQueryOptions(options);
|
|
@@ -3973,14 +4445,15 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3973
4445
|
const checkDrift = (value) => {
|
|
3974
4446
|
const mode = resolveDriftMode(resolved.sdk.drift, opts.drift);
|
|
3975
4447
|
if (mode === "off") return;
|
|
3976
|
-
|
|
4448
|
+
const partial = resolved.driverOpts.projection !== void 0;
|
|
3977
4449
|
const docs = shape === "many" ? value : shape === "one" && value !== null ? [value] : [];
|
|
3978
4450
|
checkDocsDrift(
|
|
3979
4451
|
{
|
|
3980
4452
|
logger,
|
|
3981
4453
|
db: dbLabel,
|
|
3982
4454
|
drift: mode,
|
|
3983
|
-
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
4455
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {},
|
|
4456
|
+
partial
|
|
3984
4457
|
},
|
|
3985
4458
|
String(collection),
|
|
3986
4459
|
opName,
|
|
@@ -4009,14 +4482,17 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
4009
4482
|
}
|
|
4010
4483
|
return payloadHash;
|
|
4011
4484
|
};
|
|
4012
|
-
const key = useCache ? opts.cache.key(
|
|
4485
|
+
const key = useCache ? opts.cache.key(
|
|
4486
|
+
cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace),
|
|
4487
|
+
getPayloadHash()
|
|
4488
|
+
) : void 0;
|
|
4013
4489
|
const fetchAndCheck = () => executeWrapped().then((value) => {
|
|
4014
4490
|
checkDrift(value);
|
|
4015
4491
|
return value;
|
|
4016
4492
|
});
|
|
4017
4493
|
const runOnce = () => {
|
|
4018
4494
|
if (useCache && key) {
|
|
4019
|
-
const colKey = cacheCollectionKey(client.databaseName, physical);
|
|
4495
|
+
const colKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
|
|
4020
4496
|
const versions = { [colKey]: opts.cache.versionOf(colKey) };
|
|
4021
4497
|
const hit = opts.cache.get(key);
|
|
4022
4498
|
if (hit !== void 0) return Promise.resolve(hit);
|
|
@@ -4028,7 +4504,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
4028
4504
|
return fetchAndCheck();
|
|
4029
4505
|
};
|
|
4030
4506
|
if (shouldDedupe) {
|
|
4031
|
-
const colKey = cacheCollectionKey(client.databaseName, physical);
|
|
4507
|
+
const colKey = cacheCollectionKey(client.databaseName, physical, opts.cacheNamespace);
|
|
4032
4508
|
const gen = opts.cache !== void 0 ? opts.cache.versionOf(colKey) : "";
|
|
4033
4509
|
return await opts.inFlight.run(`${colKey}|${gen}|${getPayloadHash()}`, runOnce);
|
|
4034
4510
|
}
|
|
@@ -4058,6 +4534,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
4058
4534
|
stampCreate,
|
|
4059
4535
|
stampUpdate,
|
|
4060
4536
|
stampReplace,
|
|
4537
|
+
validateWrite,
|
|
4061
4538
|
read,
|
|
4062
4539
|
normalizeFindOptions
|
|
4063
4540
|
};
|
|
@@ -4282,7 +4759,16 @@ var makeWatchOps = (ctx) => {
|
|
|
4282
4759
|
// src/service/crud/write-ops.ts
|
|
4283
4760
|
var makeWriteOps = (ctx, readOps) => {
|
|
4284
4761
|
const { opts } = ctx;
|
|
4285
|
-
const {
|
|
4762
|
+
const {
|
|
4763
|
+
coll,
|
|
4764
|
+
toDriverFilter,
|
|
4765
|
+
writeRun,
|
|
4766
|
+
invalidate,
|
|
4767
|
+
stampCreate,
|
|
4768
|
+
stampUpdate,
|
|
4769
|
+
stampReplace,
|
|
4770
|
+
validateWrite
|
|
4771
|
+
} = ctx;
|
|
4286
4772
|
const { getOne } = readOps;
|
|
4287
4773
|
const safeAfterHooks = async (collection, name, hookCtx) => {
|
|
4288
4774
|
try {
|
|
@@ -4304,6 +4790,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
4304
4790
|
doc
|
|
4305
4791
|
});
|
|
4306
4792
|
stampCreate(collection, doc);
|
|
4793
|
+
validateWrite(collection, doc);
|
|
4307
4794
|
const result = await writeRun(
|
|
4308
4795
|
collection,
|
|
4309
4796
|
"mongo.insertOne",
|
|
@@ -4337,7 +4824,10 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
4337
4824
|
collection: String(collection),
|
|
4338
4825
|
docs
|
|
4339
4826
|
});
|
|
4340
|
-
for (const d of docs)
|
|
4827
|
+
for (const d of docs) {
|
|
4828
|
+
stampCreate(collection, d);
|
|
4829
|
+
validateWrite(collection, d);
|
|
4830
|
+
}
|
|
4341
4831
|
const result = await writeRun(
|
|
4342
4832
|
collection,
|
|
4343
4833
|
"mongo.insertMany",
|
|
@@ -4461,6 +4951,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
4461
4951
|
filter
|
|
4462
4952
|
});
|
|
4463
4953
|
const effective = stampReplace(collection, replacement);
|
|
4954
|
+
validateWrite(collection, effective, false);
|
|
4464
4955
|
const result = await writeRun(
|
|
4465
4956
|
collection,
|
|
4466
4957
|
"mongo.findOneAndReplace",
|
|
@@ -4494,6 +4985,8 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
4494
4985
|
collection: String(collection),
|
|
4495
4986
|
filter
|
|
4496
4987
|
});
|
|
4988
|
+
const effective = stampReplace(collection, replacement);
|
|
4989
|
+
validateWrite(collection, effective, false);
|
|
4497
4990
|
const result = await writeRun(
|
|
4498
4991
|
collection,
|
|
4499
4992
|
"mongo.replaceOne",
|
|
@@ -4504,7 +4997,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
4504
4997
|
};
|
|
4505
4998
|
return coll(collection).replaceOne(
|
|
4506
4999
|
toDriverFilter(filter),
|
|
4507
|
-
|
|
5000
|
+
effective,
|
|
4508
5001
|
repOpts
|
|
4509
5002
|
);
|
|
4510
5003
|
},
|
|
@@ -4760,6 +5253,7 @@ var makeCrudOps = (client, dbLabel, logger, opts) => {
|
|
|
4760
5253
|
...makeWatchOps(ctx)
|
|
4761
5254
|
};
|
|
4762
5255
|
};
|
|
5256
|
+
var { EJSON: EJSON2 } = BSON;
|
|
4763
5257
|
var tag = (value) => {
|
|
4764
5258
|
if (value instanceof ObjectId) return { t: "oid", v: value.toHexString() };
|
|
4765
5259
|
if (value instanceof Date) return { t: "date", v: value.toISOString() };
|
|
@@ -4767,6 +5261,9 @@ var tag = (value) => {
|
|
|
4767
5261
|
if (typeof value === "number") return { t: "number", v: value };
|
|
4768
5262
|
if (typeof value === "boolean") return { t: "boolean", v: value };
|
|
4769
5263
|
if (value === null || value === void 0) return { t: "null" };
|
|
5264
|
+
if (typeof value === "object" && "_bsontype" in value) {
|
|
5265
|
+
return { t: "bson", v: EJSON2.stringify(value, { relaxed: false }) };
|
|
5266
|
+
}
|
|
4770
5267
|
throw new BadRequest(
|
|
4771
5268
|
`paginateCursor: unsupported sort value type "${typeof value}" \u2014 key values must be ObjectId, Date, string, number, boolean, or null`
|
|
4772
5269
|
);
|
|
@@ -4800,6 +5297,11 @@ var untag = (tagged) => {
|
|
|
4800
5297
|
return tagged.v;
|
|
4801
5298
|
case "null":
|
|
4802
5299
|
return null;
|
|
5300
|
+
case "bson":
|
|
5301
|
+
if (typeof tagged.v !== "string") {
|
|
5302
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 BSON value must be a string");
|
|
5303
|
+
}
|
|
5304
|
+
return EJSON2.parse(tagged.v, { relaxed: false });
|
|
4803
5305
|
}
|
|
4804
5306
|
};
|
|
4805
5307
|
var encodeCursor = (cursor) => Buffer.from(
|
|
@@ -4844,11 +5346,17 @@ var makePaginateCursor = (ctx) => {
|
|
|
4844
5346
|
const { client, dbLabel, logger, opts, resolve, deps: deps2 } = ctx;
|
|
4845
5347
|
const paginateCursor = async (collection, baseFilter, config) => {
|
|
4846
5348
|
const sortKeys = Object.entries(config.sort);
|
|
5349
|
+
const uniqueTiebreaker = config.uniqueTiebreaker ?? "_id";
|
|
4847
5350
|
if (sortKeys.length === 0) {
|
|
4848
5351
|
throw new BadRequest(
|
|
4849
5352
|
"paginateCursor: `sort` is required (include a unique tiebreaker like `_id`)"
|
|
4850
5353
|
);
|
|
4851
5354
|
}
|
|
5355
|
+
if (sortKeys.at(-1)?.[0] !== uniqueTiebreaker) {
|
|
5356
|
+
throw new BadRequest(
|
|
5357
|
+
`paginateCursor: the final sort key must be the unique tiebreaker "${uniqueTiebreaker}"`
|
|
5358
|
+
);
|
|
5359
|
+
}
|
|
4852
5360
|
if (config.before !== void 0 && config.after !== void 0) {
|
|
4853
5361
|
throw new BadRequest("paginateCursor: provide either `after` or `before`, not both");
|
|
4854
5362
|
}
|
|
@@ -4904,10 +5412,15 @@ var makePaginateCursor = (ctx) => {
|
|
|
4904
5412
|
"mongo.paginateCursor",
|
|
4905
5413
|
page
|
|
4906
5414
|
);
|
|
4907
|
-
const boundary = backwards ?
|
|
5415
|
+
const boundary = backwards ? page[0] : page[page.length - 1];
|
|
4908
5416
|
let nextCursor = null;
|
|
4909
5417
|
if (hasMore && boundary !== void 0) {
|
|
4910
|
-
const values = sortKeys.map(
|
|
5418
|
+
const values = sortKeys.map(
|
|
5419
|
+
([f]) => f.split(".").reduce(
|
|
5420
|
+
(value, part) => value !== null && typeof value === "object" ? value[part] : void 0,
|
|
5421
|
+
boundary
|
|
5422
|
+
)
|
|
5423
|
+
);
|
|
4911
5424
|
if (values.some((v) => v === void 0)) {
|
|
4912
5425
|
throw new BadRequest(
|
|
4913
5426
|
"paginateCursor: boundary document is missing a sort field \u2014 every sort key must exist on every document for keyset pagination"
|
|
@@ -4934,6 +5447,12 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4934
5447
|
// APIs used to disagree silently (10 vs 100).
|
|
4935
5448
|
{ limit: DEFAULT_FIND_LIMIT }
|
|
4936
5449
|
);
|
|
5450
|
+
const maxOffset = config.maxOffset ?? 1e4;
|
|
5451
|
+
if (normalized.offset > maxOffset) {
|
|
5452
|
+
throw new BadRequest(
|
|
5453
|
+
`paginateFlexible: offset ${normalized.offset} exceeds maxOffset ${maxOffset}; use paginateCursor for deep pages`
|
|
5454
|
+
);
|
|
5455
|
+
}
|
|
4937
5456
|
const dataStages = [
|
|
4938
5457
|
...config.prePipeline ?? [],
|
|
4939
5458
|
...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
|
|
@@ -4968,19 +5487,18 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4968
5487
|
const row = rows[0];
|
|
4969
5488
|
const data = row?.data ?? [];
|
|
4970
5489
|
const totalCount = row?.totalCount?.[0]?.count ?? 0;
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
}
|
|
5490
|
+
checkDocsDrift(
|
|
5491
|
+
{
|
|
5492
|
+
logger,
|
|
5493
|
+
db: dbLabel,
|
|
5494
|
+
drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
|
|
5495
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {},
|
|
5496
|
+
partial: config.projection !== void 0
|
|
5497
|
+
},
|
|
5498
|
+
String(collection),
|
|
5499
|
+
"mongo.paginateFlexible",
|
|
5500
|
+
data
|
|
5501
|
+
);
|
|
4984
5502
|
return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
|
|
4985
5503
|
}
|
|
4986
5504
|
});
|
|
@@ -4991,7 +5509,13 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4991
5509
|
// src/service/pagination/index.ts
|
|
4992
5510
|
var makePaginationOps = (client, dbLabel, logger, opts) => {
|
|
4993
5511
|
const resolve = opts.resolveCollectionName;
|
|
4994
|
-
const deps2 = makeOpDeps(
|
|
5512
|
+
const deps2 = makeOpDeps(
|
|
5513
|
+
logger,
|
|
5514
|
+
dbLabel,
|
|
5515
|
+
resolve,
|
|
5516
|
+
opts.wrapMongoErrors === true,
|
|
5517
|
+
opts.debugQuery
|
|
5518
|
+
);
|
|
4995
5519
|
const ctx = {
|
|
4996
5520
|
client,
|
|
4997
5521
|
dbLabel,
|
|
@@ -5008,16 +5532,37 @@ var makePaginationOps = (client, dbLabel, logger, opts) => {
|
|
|
5008
5532
|
};
|
|
5009
5533
|
|
|
5010
5534
|
// src/service/schema-ops.ts
|
|
5011
|
-
var optionSignature = (
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5535
|
+
var optionSignature = (options) => {
|
|
5536
|
+
const normalized = {
|
|
5537
|
+
unique: Boolean(options?.unique),
|
|
5538
|
+
sparse: Boolean(options?.sparse),
|
|
5539
|
+
hidden: Boolean(options?.hidden),
|
|
5540
|
+
expireAfterSeconds: typeof options?.expireAfterSeconds === "number" ? options.expireAfterSeconds : null
|
|
5541
|
+
};
|
|
5542
|
+
for (const key of [
|
|
5543
|
+
"partialFilterExpression",
|
|
5544
|
+
"collation",
|
|
5545
|
+
"wildcardProjection",
|
|
5546
|
+
"weights",
|
|
5547
|
+
"default_language",
|
|
5548
|
+
"language_override",
|
|
5549
|
+
"textIndexVersion",
|
|
5550
|
+
"2dsphereIndexVersion",
|
|
5551
|
+
"bits",
|
|
5552
|
+
"min",
|
|
5553
|
+
"max",
|
|
5554
|
+
"bucketSize",
|
|
5555
|
+
"storageEngine"
|
|
5556
|
+
]) {
|
|
5557
|
+
if (options?.[key] !== void 0) normalized[key] = options[key];
|
|
5558
|
+
}
|
|
5559
|
+
return stableStringify(normalized);
|
|
5560
|
+
};
|
|
5016
5561
|
var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
|
|
5017
5562
|
var makeSchemaOps = (client, _logger, opts) => {
|
|
5018
|
-
const
|
|
5563
|
+
const readCollectionValidation = async (physical) => {
|
|
5019
5564
|
const info = await client.listCollections({ name: physical }).next();
|
|
5020
|
-
return info?.options
|
|
5565
|
+
return info?.options ?? {};
|
|
5021
5566
|
};
|
|
5022
5567
|
const ensureDeclaredIndexes = async (collection, physical, def) => {
|
|
5023
5568
|
const declared = def?.indexes ?? [];
|
|
@@ -5031,7 +5576,7 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
5031
5576
|
try {
|
|
5032
5577
|
await handle.createIndex(index.key, index.options);
|
|
5033
5578
|
} catch (err) {
|
|
5034
|
-
if (err?.code
|
|
5579
|
+
if ([85, 86].includes(err?.code ?? -1)) {
|
|
5035
5580
|
throw new Error(
|
|
5036
5581
|
`Cannot apply declared index on "${collection}": an index with the same name/key already exists with different options. Run db.syncIndexes("${collection}") to reconcile index drift.`,
|
|
5037
5582
|
{ cause: err }
|
|
@@ -5056,19 +5601,21 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
5056
5601
|
created = false;
|
|
5057
5602
|
}
|
|
5058
5603
|
if (created) {
|
|
5059
|
-
|
|
5060
|
-
await ensureDeclaredIndexes(collection, physical, def);
|
|
5061
|
-
} catch (err) {
|
|
5062
|
-
await client.dropCollection(physical).catch(() => {
|
|
5063
|
-
});
|
|
5064
|
-
throw err;
|
|
5065
|
-
}
|
|
5604
|
+
await ensureDeclaredIndexes(collection, physical, def);
|
|
5066
5605
|
return;
|
|
5067
5606
|
}
|
|
5068
5607
|
if (createOptions.validator) {
|
|
5069
|
-
const current = await
|
|
5070
|
-
|
|
5071
|
-
|
|
5608
|
+
const current = await readCollectionValidation(physical);
|
|
5609
|
+
const collMod = {};
|
|
5610
|
+
if (stableStringify(current.validator) !== stableStringify(createOptions.validator)) {
|
|
5611
|
+
collMod.validator = createOptions.validator;
|
|
5612
|
+
}
|
|
5613
|
+
for (const key of ["validationLevel", "validationAction"]) {
|
|
5614
|
+
const requested = createOptions[key];
|
|
5615
|
+
if (requested !== void 0 && current[key] !== requested) collMod[key] = requested;
|
|
5616
|
+
}
|
|
5617
|
+
if (Object.keys(collMod).length > 0) {
|
|
5618
|
+
await client.command({ collMod: physical, ...collMod });
|
|
5072
5619
|
}
|
|
5073
5620
|
}
|
|
5074
5621
|
await ensureDeclaredIndexes(collection, physical, def);
|
|
@@ -5084,6 +5631,7 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
5084
5631
|
collMod: physical,
|
|
5085
5632
|
validator: toMongoValidator(resolved)
|
|
5086
5633
|
});
|
|
5634
|
+
if (def) def.schema = resolved;
|
|
5087
5635
|
};
|
|
5088
5636
|
const syncIndexes = async (collection) => {
|
|
5089
5637
|
const physical = opts.resolveCollectionName(collection);
|
|
@@ -5097,52 +5645,68 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
5097
5645
|
const declaredByKey = new Map(
|
|
5098
5646
|
declared.map((d) => [indexKeyOf(d.key, d.options), d])
|
|
5099
5647
|
);
|
|
5100
|
-
const
|
|
5648
|
+
const keyPattern = (key) => JSON.stringify(Object.entries(key));
|
|
5101
5649
|
const dropped = [];
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
if (!existingByKey.has(key)) {
|
|
5116
|
-
created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
|
|
5650
|
+
const droppedNames = /* @__PURE__ */ new Set();
|
|
5651
|
+
for (const spec of declaredByKey.values()) {
|
|
5652
|
+
const signature = indexKeyOf(spec.key, spec.options);
|
|
5653
|
+
if (existingByKey.has(signature)) continue;
|
|
5654
|
+
for (const index of existing) {
|
|
5655
|
+
const name = index.name;
|
|
5656
|
+
if (droppedNames.has(name)) continue;
|
|
5657
|
+
const sameName = spec.options?.name !== void 0 && index.name === spec.options.name;
|
|
5658
|
+
if (sameName || keyPattern(index.key) === keyPattern(spec.key)) {
|
|
5659
|
+
await handle.dropIndex(name);
|
|
5660
|
+
droppedNames.add(name);
|
|
5661
|
+
dropped.push(name);
|
|
5662
|
+
}
|
|
5117
5663
|
}
|
|
5664
|
+
await handle.createIndex(spec.key, spec.options);
|
|
5118
5665
|
}
|
|
5119
|
-
for (const [
|
|
5120
|
-
if (!declaredByKey.has(
|
|
5666
|
+
for (const [signature, name] of existingByKey) {
|
|
5667
|
+
if (!declaredByKey.has(signature) && !droppedNames.has(name)) {
|
|
5121
5668
|
await handle.dropIndex(name);
|
|
5122
5669
|
dropped.push(name);
|
|
5123
5670
|
}
|
|
5124
5671
|
}
|
|
5672
|
+
const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
5673
|
+
const afterByKey = new Map(
|
|
5674
|
+
after.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5675
|
+
);
|
|
5676
|
+
const created = [...declaredByKey].filter(([signature]) => !existingByKey.has(signature)).map(
|
|
5677
|
+
([signature, spec]) => spec.options?.name ?? afterByKey.get(signature) ?? JSON.stringify(spec.key)
|
|
5678
|
+
);
|
|
5125
5679
|
return { created, dropped };
|
|
5126
5680
|
};
|
|
5127
5681
|
return { createSchema, updateSchema, syncIndexes };
|
|
5128
5682
|
};
|
|
5129
|
-
|
|
5130
|
-
// src/service/transaction.ts
|
|
5131
5683
|
var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
5132
5684
|
let warnedUnsupported = false;
|
|
5133
5685
|
let journalIndexReady = false;
|
|
5134
5686
|
const MIGRATE_LEASE_MS = 12e4;
|
|
5135
|
-
const transaction = async (fn, options) => {
|
|
5687
|
+
const transaction = async (fn, options = {}) => {
|
|
5688
|
+
const { allowNonTransactionalFallback, ...driverOptions } = options;
|
|
5689
|
+
const allowFallback = allowNonTransactionalFallback ?? opts.allowNonTransactionalFallback ?? false;
|
|
5136
5690
|
const session = client.startSession();
|
|
5137
5691
|
try {
|
|
5138
|
-
return await session.withTransaction(async () => fn(session),
|
|
5692
|
+
return await session.withTransaction(async () => fn(session), driverOptions);
|
|
5139
5693
|
} catch (err) {
|
|
5140
5694
|
if (isTransactionUnsupportedError(err)) {
|
|
5695
|
+
if (!allowFallback) {
|
|
5696
|
+
if (opts.wrapMongoErrors) {
|
|
5697
|
+
throw new InfraError(
|
|
5698
|
+
"TRANSACTIONS_UNSUPPORTED",
|
|
5699
|
+
"MongoDB deployment does not support transactions; enable allowNonTransactionalFallback to run non-atomically",
|
|
5700
|
+
{ db: handle.databaseName, op: "transaction" }
|
|
5701
|
+
);
|
|
5702
|
+
}
|
|
5703
|
+
throw err;
|
|
5704
|
+
}
|
|
5141
5705
|
if (!warnedUnsupported) {
|
|
5142
5706
|
warnedUnsupported = true;
|
|
5143
5707
|
logger.warn?.(
|
|
5144
5708
|
{ error: err instanceof Error ? err.message : String(err) },
|
|
5145
|
-
"transactions unsupported
|
|
5709
|
+
"transactions unsupported; running explicitly requested non-transactional fallback"
|
|
5146
5710
|
);
|
|
5147
5711
|
}
|
|
5148
5712
|
return fn(null);
|
|
@@ -5152,16 +5716,14 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5152
5716
|
}
|
|
5153
5717
|
throw err;
|
|
5154
5718
|
} finally {
|
|
5155
|
-
await session.endSession()
|
|
5719
|
+
await session.endSession().catch(() => {
|
|
5720
|
+
});
|
|
5156
5721
|
}
|
|
5157
5722
|
};
|
|
5158
|
-
const migrate = async (name, fn, filePath) => {
|
|
5723
|
+
const migrate = async (name, fn, filePath, options = {}) => {
|
|
5159
5724
|
const journal = handle.collection(opts.migrationCollectionPhysical);
|
|
5160
5725
|
if (!journalIndexReady) {
|
|
5161
|
-
|
|
5162
|
-
await journal.createIndex({ name: 1 }, { unique: true });
|
|
5163
|
-
} catch {
|
|
5164
|
-
}
|
|
5726
|
+
await journal.createIndex({ name: 1 }, { unique: true });
|
|
5165
5727
|
journalIndexReady = true;
|
|
5166
5728
|
}
|
|
5167
5729
|
const isDupKeyError = (err) => {
|
|
@@ -5173,10 +5735,11 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5173
5735
|
const sessionOpts = session ? { session } : {};
|
|
5174
5736
|
const now = /* @__PURE__ */ new Date();
|
|
5175
5737
|
const expiresAt = new Date(Date.now() + MIGRATE_LEASE_MS);
|
|
5738
|
+
const claimId = randomUUID();
|
|
5176
5739
|
const stole = await journal.findOneAndUpdate(
|
|
5177
5740
|
{
|
|
5178
5741
|
name,
|
|
5179
|
-
status: { $exists: true, $nin: ["applied"] },
|
|
5742
|
+
status: { $exists: true, $nin: ["applied", "rolling_back"] },
|
|
5180
5743
|
$or: [
|
|
5181
5744
|
{ expiresAt: { $lt: now } },
|
|
5182
5745
|
{
|
|
@@ -5190,13 +5753,14 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5190
5753
|
status: "running",
|
|
5191
5754
|
appliedAt: now,
|
|
5192
5755
|
expiresAt,
|
|
5756
|
+
claimId,
|
|
5193
5757
|
...filePath ? { file: filePath } : {}
|
|
5194
5758
|
}
|
|
5195
5759
|
},
|
|
5196
5760
|
{ returnDocument: "before", ...sessionOpts }
|
|
5197
5761
|
);
|
|
5198
5762
|
if (stole !== null) {
|
|
5199
|
-
await runClaimed(session);
|
|
5763
|
+
await runClaimed(session, claimId);
|
|
5200
5764
|
return;
|
|
5201
5765
|
}
|
|
5202
5766
|
try {
|
|
@@ -5206,6 +5770,7 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5206
5770
|
appliedAt: now,
|
|
5207
5771
|
status: "running",
|
|
5208
5772
|
expiresAt,
|
|
5773
|
+
claimId,
|
|
5209
5774
|
...filePath ? { file: filePath } : {}
|
|
5210
5775
|
},
|
|
5211
5776
|
sessionOpts
|
|
@@ -5224,13 +5789,13 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5224
5789
|
}
|
|
5225
5790
|
return;
|
|
5226
5791
|
}
|
|
5227
|
-
await runClaimed(session);
|
|
5228
|
-
});
|
|
5229
|
-
async function runClaimed(session) {
|
|
5792
|
+
await runClaimed(session, claimId);
|
|
5793
|
+
}, options);
|
|
5794
|
+
async function runClaimed(session, claimId) {
|
|
5230
5795
|
const sessionOpts = session ? { session } : {};
|
|
5231
5796
|
const heartbeat = session === null ? setInterval(() => {
|
|
5232
5797
|
void journal.updateOne(
|
|
5233
|
-
{ name, status: "running" },
|
|
5798
|
+
{ name, claimId, status: "running" },
|
|
5234
5799
|
{ $set: { expiresAt: new Date(Date.now() + MIGRATE_LEASE_MS) } }
|
|
5235
5800
|
).catch(() => {
|
|
5236
5801
|
});
|
|
@@ -5239,16 +5804,15 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5239
5804
|
try {
|
|
5240
5805
|
await fn(session);
|
|
5241
5806
|
const res = await journal.updateOne(
|
|
5242
|
-
{ name, status: "running" },
|
|
5243
|
-
{ $set: { status: "applied" } },
|
|
5807
|
+
{ name, claimId, status: "running" },
|
|
5808
|
+
{ $set: { status: "applied" }, $unset: { expiresAt: "", claimId: "" } },
|
|
5244
5809
|
sessionOpts
|
|
5245
5810
|
);
|
|
5246
|
-
if (res
|
|
5247
|
-
|
|
5248
|
-
});
|
|
5811
|
+
if (res.matchedCount === 0) {
|
|
5812
|
+
throw new Error(`Migration "${name}" lost its claim before it could be marked applied`);
|
|
5249
5813
|
}
|
|
5250
5814
|
} catch (err) {
|
|
5251
|
-
await journal.deleteOne({ name }, sessionOpts).catch(() => {
|
|
5815
|
+
await journal.deleteOne({ name, claimId, status: "running" }, sessionOpts).catch(() => {
|
|
5252
5816
|
});
|
|
5253
5817
|
throw err;
|
|
5254
5818
|
} finally {
|
|
@@ -5260,7 +5824,7 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
5260
5824
|
};
|
|
5261
5825
|
|
|
5262
5826
|
// src/service/manager.ts
|
|
5263
|
-
var buildManager = (deps2, dbKey, dbName, client) => {
|
|
5827
|
+
var buildManager = (deps2, dbKey, dbName, client, cacheNamespace = "") => {
|
|
5264
5828
|
const { dbClients, config, logger, sharedCache, dedupeReads, inFlight, collectionRefs } = deps2;
|
|
5265
5829
|
const definition = dbClients[dbKey];
|
|
5266
5830
|
const handle = client.db(dbName);
|
|
@@ -5273,7 +5837,11 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
5273
5837
|
registry.set(logical, def);
|
|
5274
5838
|
hooks[logical] = def.hooks;
|
|
5275
5839
|
if (def.timestamps) timestamps[logical] = def.timestamps;
|
|
5276
|
-
collectionRefs.push({
|
|
5840
|
+
collectionRefs.push({
|
|
5841
|
+
db: handle,
|
|
5842
|
+
collection: baseResolveCollectionName(logical),
|
|
5843
|
+
namespace: cacheNamespace
|
|
5844
|
+
});
|
|
5277
5845
|
}
|
|
5278
5846
|
const getDefinition = (logical) => registry.get(logical);
|
|
5279
5847
|
const resolveCollectionName = (logical) => {
|
|
@@ -5284,18 +5852,23 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
5284
5852
|
};
|
|
5285
5853
|
const crud = makeCrudOps(handle, dbName, logger, {
|
|
5286
5854
|
resolveCollectionName,
|
|
5855
|
+
cacheNamespace,
|
|
5287
5856
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
5857
|
+
...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
|
|
5288
5858
|
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
5289
5859
|
dedupeReads,
|
|
5290
5860
|
inFlight,
|
|
5291
5861
|
hooks,
|
|
5292
5862
|
timestamps,
|
|
5863
|
+
...deps2.validateWrites !== void 0 ? { validateWrites: deps2.validateWrites } : {},
|
|
5293
5864
|
...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
|
|
5294
5865
|
...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {}
|
|
5295
5866
|
});
|
|
5296
5867
|
const pagination = makePaginationOps(handle, dbName, logger, {
|
|
5297
5868
|
resolveCollectionName,
|
|
5869
|
+
cacheNamespace,
|
|
5298
5870
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
5871
|
+
...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
|
|
5299
5872
|
...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
|
|
5300
5873
|
...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {},
|
|
5301
5874
|
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
@@ -5304,7 +5877,9 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
5304
5877
|
});
|
|
5305
5878
|
const aggregation = makeAggregationOps(handle, dbName, logger, {
|
|
5306
5879
|
resolveCollectionName,
|
|
5880
|
+
cacheNamespace,
|
|
5307
5881
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
5882
|
+
...deps2.debugQuery !== void 0 ? { debugQuery: deps2.debugQuery } : {},
|
|
5308
5883
|
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
5309
5884
|
dedupeReads,
|
|
5310
5885
|
inFlight
|
|
@@ -5312,16 +5887,14 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
5312
5887
|
const schema = makeSchemaOps(handle, logger, { resolveCollectionName, getDefinition });
|
|
5313
5888
|
const transactionOps = makeTransactionOps(client, handle, logger, config, {
|
|
5314
5889
|
migrationCollectionPhysical: resolveCollectionName("_migrations"),
|
|
5315
|
-
wrapMongoErrors: deps2.wrapMongoErrors
|
|
5890
|
+
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
5891
|
+
...deps2.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: deps2.allowNonTransactionalFallback } : {}
|
|
5316
5892
|
});
|
|
5317
5893
|
const populator = makePopulator({
|
|
5318
|
-
findMany: async (logical, filter, options) => {
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
limit: options?.limit ?? 1e4
|
|
5323
|
-
}).toArray();
|
|
5324
|
-
}
|
|
5894
|
+
findMany: async (logical, filter, options) => crud.findMany(logical, filter, {
|
|
5895
|
+
...options,
|
|
5896
|
+
cache: options?.queryCache ?? false
|
|
5897
|
+
})
|
|
5325
5898
|
});
|
|
5326
5899
|
const populate = (async (docs, relations, options = {}) => {
|
|
5327
5900
|
const result = await populator.populate(docs, relations, options);
|
|
@@ -5335,6 +5908,7 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
5335
5908
|
...transactionOps,
|
|
5336
5909
|
populate,
|
|
5337
5910
|
client: handle,
|
|
5911
|
+
mongoClient: client,
|
|
5338
5912
|
db: deps2.db
|
|
5339
5913
|
};
|
|
5340
5914
|
};
|
|
@@ -5362,25 +5936,44 @@ var createMongoService = (dbClients, configOverrides = {}) => {
|
|
|
5362
5936
|
dedupeReads,
|
|
5363
5937
|
inFlight,
|
|
5364
5938
|
wrapMongoErrors: runtime.wrapMongoErrors,
|
|
5939
|
+
...configOverrides.debugQuery !== void 0 ? { debugQuery: configOverrides.debugQuery } : {},
|
|
5940
|
+
...configOverrides.allowNonTransactionalFallback !== void 0 ? { allowNonTransactionalFallback: configOverrides.allowNonTransactionalFallback } : {},
|
|
5941
|
+
...configOverrides.validateWrites !== void 0 ? { validateWrites: configOverrides.validateWrites } : {},
|
|
5365
5942
|
...configOverrides.drift !== void 0 ? { drift: configOverrides.drift } : {},
|
|
5366
5943
|
collectionRefs,
|
|
5367
5944
|
db
|
|
5368
5945
|
},
|
|
5369
5946
|
key,
|
|
5370
5947
|
dbName,
|
|
5371
|
-
client
|
|
5948
|
+
client,
|
|
5949
|
+
stableHash(getDbUrl(key))
|
|
5372
5950
|
)),
|
|
5373
5951
|
openedClients
|
|
5374
5952
|
);
|
|
5375
|
-
|
|
5953
|
+
let lifecycle = Promise.resolve();
|
|
5954
|
+
const serializeLifecycle = (operation) => {
|
|
5955
|
+
const result = lifecycle.then(operation, operation);
|
|
5956
|
+
lifecycle = result.then(
|
|
5957
|
+
() => {
|
|
5958
|
+
},
|
|
5959
|
+
() => {
|
|
5960
|
+
}
|
|
5961
|
+
);
|
|
5962
|
+
return result;
|
|
5963
|
+
};
|
|
5964
|
+
const makeConnections = (names) => serializeLifecycle(async () => {
|
|
5376
5965
|
await baseMakeConnections(names);
|
|
5377
5966
|
await cacheInvalidator?.start(collectionRefs);
|
|
5378
|
-
};
|
|
5379
|
-
const closeConnections2 = async () => {
|
|
5967
|
+
});
|
|
5968
|
+
const closeConnections2 = () => serializeLifecycle(async () => {
|
|
5380
5969
|
await cacheInvalidator?.stop();
|
|
5381
5970
|
collectionRefs.length = 0;
|
|
5382
|
-
|
|
5383
|
-
|
|
5971
|
+
try {
|
|
5972
|
+
await closeConnections(openedClients);
|
|
5973
|
+
} finally {
|
|
5974
|
+
for (const key of Object.keys(db)) delete db[key];
|
|
5975
|
+
}
|
|
5976
|
+
});
|
|
5384
5977
|
const eachDb = async (fn) => {
|
|
5385
5978
|
return Promise.all(Object.keys(db).map((key) => fn(db[key], key.replace(/Client$/, ""))));
|
|
5386
5979
|
};
|