@ignex/ninox 0.1.3 → 0.1.5
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 +51 -11
- package/dist/index.d.ts +204 -70
- package/dist/index.js +1341 -427
- package/dist/index.js.map +1 -1
- package/dist/{timeout-BgrqtMnd.d.ts → timeout-B10CTiGm.d.ts} +24 -4
- package/dist/utils.d.ts +32 -8
- package/dist/utils.js +81 -11
- package/dist/utils.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObjectId,
|
|
1
|
+
import { ObjectId, Decimal128, Long, Int32, Double, Timestamp, Binary, MinKey, MaxKey, DBRef, Code, MongoClient } from 'mongodb';
|
|
2
2
|
import { writeFile, mkdir, readdir } from 'fs/promises';
|
|
3
3
|
import { basename, join } from 'path';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
@@ -16,8 +16,11 @@ var withTimeout = (promise, timeoutMs, label) => {
|
|
|
16
16
|
if (timer !== void 0) clearTimeout(timer);
|
|
17
17
|
});
|
|
18
18
|
};
|
|
19
|
-
var sleep = (ms) => new Promise((
|
|
20
|
-
|
|
19
|
+
var sleep = (ms, unref = false) => new Promise((resolve) => {
|
|
20
|
+
const timer = setTimeout(resolve, ms);
|
|
21
|
+
if (unref) timer.unref?.();
|
|
22
|
+
});
|
|
23
|
+
var sleepJittered = (ms, jitterMs = 1e3, unref = false) => sleep(ms + Math.floor(Math.random() * Math.min(ms, jitterMs)), unref);
|
|
21
24
|
|
|
22
25
|
// src/capabilities.ts
|
|
23
26
|
var createMongoCapabilitiesStore = () => {
|
|
@@ -53,14 +56,6 @@ var probeMongoCapabilities = async (client) => {
|
|
|
53
56
|
return { probed: false, transactionsSupported: false };
|
|
54
57
|
}
|
|
55
58
|
};
|
|
56
|
-
var mongoTransactionsEnabled = (store, options = {}) => {
|
|
57
|
-
const env = options.envOverride ?? readMongoTransactionsEnvOverride();
|
|
58
|
-
if (env !== void 0) return env;
|
|
59
|
-
const state = store.get();
|
|
60
|
-
if (state.probed) return state.transactionsSupported;
|
|
61
|
-
if (options.urlHint) return /replicaSet=/.test(options.urlHint);
|
|
62
|
-
return false;
|
|
63
|
-
};
|
|
64
59
|
|
|
65
60
|
// src/errors/classes.ts
|
|
66
61
|
var ERROR_HTTP_STATUS = {
|
|
@@ -82,7 +77,7 @@ var AppError = class extends Error {
|
|
|
82
77
|
this.name = "AppError";
|
|
83
78
|
this.code = code;
|
|
84
79
|
this.statusCode = statusCode;
|
|
85
|
-
this.extra = extra;
|
|
80
|
+
if (extra !== void 0) this.extra = extra;
|
|
86
81
|
}
|
|
87
82
|
/**
|
|
88
83
|
* Stable, client-safe serialization for framework error middleware: a
|
|
@@ -135,8 +130,14 @@ var TRANSIENT_MONGO_ERROR_CODES = Object.freeze(
|
|
|
135
130
|
// ShutdownInProgress
|
|
136
131
|
189,
|
|
137
132
|
// PrimarySteppedDown
|
|
133
|
+
251,
|
|
134
|
+
// NoSuchTransaction (stepdown mid-transaction)
|
|
135
|
+
262,
|
|
136
|
+
// ExceededTimeLimit (part of the retryable transaction set)
|
|
138
137
|
9001,
|
|
139
138
|
// SocketException
|
|
139
|
+
10058,
|
|
140
|
+
// NotWritablePrimary (legacy server code)
|
|
140
141
|
10107,
|
|
141
142
|
// NotWritablePrimary
|
|
142
143
|
11600,
|
|
@@ -149,10 +150,13 @@ var TRANSIENT_MONGO_ERROR_CODES = Object.freeze(
|
|
|
149
150
|
// NotPrimaryOrSecondary
|
|
150
151
|
])
|
|
151
152
|
);
|
|
152
|
-
var TRANSIENT_NAME = /MongoNetworkError|PoolClearedError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoNetworkTimeoutError/;
|
|
153
|
+
var TRANSIENT_NAME = /MongoNetworkError|PoolClearedError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoNetworkTimeoutError|MongoServerSelectionError/;
|
|
153
154
|
var isMongoTransientError = (error) => {
|
|
154
155
|
if (!error || typeof error !== "object") return false;
|
|
155
156
|
const e = error;
|
|
157
|
+
if (typeof e.hasErrorLabel === "function" && e.hasErrorLabel("TransientTransactionError")) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
156
160
|
if (typeof e.name === "string" && TRANSIENT_NAME.test(e.name)) return true;
|
|
157
161
|
if (typeof e.code === "number" && TRANSIENT_MONGO_ERROR_CODES.has(e.code)) return true;
|
|
158
162
|
return false;
|
|
@@ -164,6 +168,42 @@ var isMongoDuplicateKeyError = (error) => {
|
|
|
164
168
|
if (typeof e.message === "string" && /E11000|duplicate key/i.test(e.message)) return true;
|
|
165
169
|
return false;
|
|
166
170
|
};
|
|
171
|
+
var isTransactionUnsupportedError = (error) => {
|
|
172
|
+
const code = error?.code;
|
|
173
|
+
if (code === 20) return true;
|
|
174
|
+
const message = error instanceof Error ? error.message : "";
|
|
175
|
+
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
|
+
};
|
|
177
|
+
var MONGO_ERROR_NAME = /MongoError|MongoServerError|MongoBulkWriteError|MongoNetworkError|MongoNetworkTimeoutError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoCursorExhaustedError|PoolClearedError|MongoTopologyClosedError|MongoServerSelectionError/;
|
|
178
|
+
var DRIVER_ERROR_CODES = Object.freeze(
|
|
179
|
+
/* @__PURE__ */ new Set([
|
|
180
|
+
20,
|
|
181
|
+
// IllegalOperation (transactions unsupported)
|
|
182
|
+
26,
|
|
183
|
+
// NamespaceNotFound
|
|
184
|
+
48,
|
|
185
|
+
// NamespaceExists
|
|
186
|
+
50,
|
|
187
|
+
// MaxTimeMSExpired
|
|
188
|
+
112,
|
|
189
|
+
// WriteConflict
|
|
190
|
+
121,
|
|
191
|
+
// DocumentValidationFailure
|
|
192
|
+
11e3,
|
|
193
|
+
// DuplicateKey
|
|
194
|
+
11001,
|
|
195
|
+
// DuplicateKey (legacy)
|
|
196
|
+
...TRANSIENT_MONGO_ERROR_CODES
|
|
197
|
+
])
|
|
198
|
+
);
|
|
199
|
+
var isMongoDriverError = (error) => {
|
|
200
|
+
if (!error || typeof error !== "object") return false;
|
|
201
|
+
const e = error;
|
|
202
|
+
if (typeof e.name === "string" && MONGO_ERROR_NAME.test(e.name)) return true;
|
|
203
|
+
if (typeof e.code === "number" && DRIVER_ERROR_CODES.has(e.code)) return true;
|
|
204
|
+
if (typeof e.message === "string" && /E11000 duplicate key/i.test(e.message)) return true;
|
|
205
|
+
return false;
|
|
206
|
+
};
|
|
167
207
|
|
|
168
208
|
// src/errors/driver-map.ts
|
|
169
209
|
var extractValidationPaths = (errInfo) => {
|
|
@@ -192,6 +232,7 @@ var addValidationContext = (context, errInfo) => {
|
|
|
192
232
|
var isBulkWriteError = (error) => typeof error === "object" && error !== null && "writeErrors" in error && Array.isArray(error.writeErrors);
|
|
193
233
|
var mapMongoDriverError = (error, ctx = {}) => {
|
|
194
234
|
if (error instanceof AppError) return error;
|
|
235
|
+
if (!isMongoDriverError(error)) return error;
|
|
195
236
|
const context = { ...ctx };
|
|
196
237
|
if (isMongoDuplicateKeyError(error)) {
|
|
197
238
|
const e = error;
|
|
@@ -204,7 +245,7 @@ var mapMongoDriverError = (error, ctx = {}) => {
|
|
|
204
245
|
if (first?.code === 11e3) {
|
|
205
246
|
if (first.keyPattern) context.keyPattern = first.keyPattern;
|
|
206
247
|
if (first.keyValue) context.keyValue = first.keyValue;
|
|
207
|
-
return new DomainError("DUPLICATE_KEY",
|
|
248
|
+
return new DomainError("DUPLICATE_KEY", "Duplicate key error", context);
|
|
208
249
|
}
|
|
209
250
|
if (first?.code === 121) {
|
|
210
251
|
addValidationContext(context, first.errInfo);
|
|
@@ -249,7 +290,47 @@ var serializeError = (err, ctx = {}) => {
|
|
|
249
290
|
var cloneDeep = (value) => {
|
|
250
291
|
if (value instanceof Date) return new Date(value.getTime());
|
|
251
292
|
if (value instanceof ObjectId) return new ObjectId(value.toHexString());
|
|
293
|
+
if (value instanceof Decimal128) return Decimal128.fromString(value.toString());
|
|
294
|
+
if (value instanceof Long) return Long.fromString(value.toString());
|
|
295
|
+
if (value instanceof Int32) return new Int32(value.valueOf());
|
|
296
|
+
if (value instanceof Double) return new Double(value.valueOf());
|
|
297
|
+
if (value instanceof Timestamp)
|
|
298
|
+
return Timestamp.fromBits(value.getLowBits(), value.getHighBits());
|
|
299
|
+
if (value instanceof Binary) {
|
|
300
|
+
const bytes = Buffer.from(value.buffer);
|
|
301
|
+
return new Binary(bytes, value.sub_type);
|
|
302
|
+
}
|
|
303
|
+
if (value instanceof MinKey) return new MinKey();
|
|
304
|
+
if (value instanceof MaxKey) return new MaxKey();
|
|
305
|
+
if (value instanceof DBRef) {
|
|
306
|
+
return new DBRef(value.collection, cloneDeep(value.oid), value.db);
|
|
307
|
+
}
|
|
308
|
+
if (value instanceof Code) {
|
|
309
|
+
return new Code(
|
|
310
|
+
value.code,
|
|
311
|
+
value.scope !== void 0 ? cloneDeep(value.scope) : void 0
|
|
312
|
+
);
|
|
313
|
+
}
|
|
252
314
|
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
|
|
315
|
+
if (value instanceof Map) {
|
|
316
|
+
const out = /* @__PURE__ */ new Map();
|
|
317
|
+
for (const [k, v] of value) out.set(cloneDeep(k), cloneDeep(v));
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
if (value instanceof Set) {
|
|
321
|
+
const out = /* @__PURE__ */ new Set();
|
|
322
|
+
for (const v of value) out.add(cloneDeep(v));
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
326
|
+
if (ArrayBuffer.isView(value)) {
|
|
327
|
+
const view = value;
|
|
328
|
+
if (view instanceof DataView) {
|
|
329
|
+
const bytes = view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
330
|
+
return new DataView(bytes);
|
|
331
|
+
}
|
|
332
|
+
return typeof view.slice === "function" ? view.slice() : value;
|
|
333
|
+
}
|
|
253
334
|
if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
|
|
254
335
|
if (value !== null && typeof value === "object") {
|
|
255
336
|
const out = {};
|
|
@@ -262,7 +343,10 @@ var cloneDeep = (value) => {
|
|
|
262
343
|
};
|
|
263
344
|
|
|
264
345
|
// src/utils/hash.ts
|
|
265
|
-
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
346
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date) && !(v instanceof RegExp) && // Mongo driver ObjectId (and other BSON values with a toHexString marker)
|
|
347
|
+
// must fall through to `toPrimitive` — treating them as plain objects
|
|
348
|
+
// would serialize every ObjectId as `{}` and collapse distinct values.
|
|
349
|
+
typeof v.toHexString !== "function";
|
|
266
350
|
var toPrimitive = (v) => {
|
|
267
351
|
if (v === void 0 || v === null) return null;
|
|
268
352
|
if (typeof v === "bigint") return v.toString();
|
|
@@ -287,11 +371,15 @@ var stableStringify = (value) => {
|
|
|
287
371
|
return JSON.stringify(visit(value));
|
|
288
372
|
};
|
|
289
373
|
var hashString = (input) => {
|
|
290
|
-
let
|
|
374
|
+
let h1 = 5381;
|
|
375
|
+
let h2 = 2166136261;
|
|
291
376
|
for (let i = 0; i < input.length; i++) {
|
|
292
|
-
|
|
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;
|
|
293
381
|
}
|
|
294
|
-
return (
|
|
382
|
+
return `${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
|
|
295
383
|
};
|
|
296
384
|
var stableHash = (value) => hashString(stableStringify(value));
|
|
297
385
|
|
|
@@ -299,11 +387,13 @@ var stableHash = (value) => hashString(stableStringify(value));
|
|
|
299
387
|
var LRU = class {
|
|
300
388
|
max;
|
|
301
389
|
map = /* @__PURE__ */ new Map();
|
|
390
|
+
onEvict;
|
|
302
391
|
/** Number of entries evicted to make room for new ones (capacity evictions only). */
|
|
303
392
|
evictions = 0;
|
|
304
393
|
constructor(options) {
|
|
305
394
|
if (options.max <= 0) throw new Error("LRU max must be > 0");
|
|
306
395
|
this.max = options.max;
|
|
396
|
+
this.onEvict = options.onEvict;
|
|
307
397
|
}
|
|
308
398
|
get size() {
|
|
309
399
|
return this.map.size;
|
|
@@ -325,10 +415,16 @@ var LRU = class {
|
|
|
325
415
|
if (this.map.has(key)) this.map.delete(key);
|
|
326
416
|
this.map.set(key, value);
|
|
327
417
|
if (this.map.size > this.max) {
|
|
328
|
-
const
|
|
329
|
-
if (
|
|
330
|
-
|
|
418
|
+
const next = this.map.keys().next();
|
|
419
|
+
if (next.done !== true) {
|
|
420
|
+
const evictedKey = next.value;
|
|
421
|
+
const evictedValue = this.map.get(evictedKey);
|
|
422
|
+
this.map.delete(evictedKey);
|
|
331
423
|
this.evictions++;
|
|
424
|
+
try {
|
|
425
|
+
this.onEvict?.(evictedKey, evictedValue);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
332
428
|
}
|
|
333
429
|
}
|
|
334
430
|
return this;
|
|
@@ -359,9 +455,20 @@ var InFlight = class {
|
|
|
359
455
|
run(key, fn) {
|
|
360
456
|
const existing = this.pending.get(key);
|
|
361
457
|
if (existing) return existing;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
458
|
+
let promise;
|
|
459
|
+
try {
|
|
460
|
+
promise = Promise.resolve(fn());
|
|
461
|
+
} catch (err) {
|
|
462
|
+
promise = Promise.reject(err);
|
|
463
|
+
}
|
|
464
|
+
promise.then(
|
|
465
|
+
() => {
|
|
466
|
+
this.pending.delete(key);
|
|
467
|
+
},
|
|
468
|
+
() => {
|
|
469
|
+
this.pending.delete(key);
|
|
470
|
+
}
|
|
471
|
+
);
|
|
365
472
|
this.pending.set(key, promise);
|
|
366
473
|
return promise;
|
|
367
474
|
}
|
|
@@ -369,24 +476,50 @@ var InFlight = class {
|
|
|
369
476
|
this.pending.clear();
|
|
370
477
|
}
|
|
371
478
|
};
|
|
372
|
-
var
|
|
479
|
+
var OVERHEAD_PER_KEY = 8;
|
|
480
|
+
var estimateSize = (value, seen) => {
|
|
373
481
|
if (value === null || value === void 0) return 0;
|
|
374
482
|
if (typeof value === "string") return value.length * 2;
|
|
375
483
|
if (typeof value === "number" || typeof value === "boolean") return 8;
|
|
484
|
+
if (typeof value === "bigint") return 16;
|
|
485
|
+
if (typeof value !== "object") return 0;
|
|
376
486
|
if (value instanceof ObjectId) return 16;
|
|
377
487
|
if (value instanceof Date) return 8;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
488
|
+
const visit = (v) => {
|
|
489
|
+
if (v !== null && typeof v === "object") {
|
|
490
|
+
if (!seen) seen = /* @__PURE__ */ new WeakSet();
|
|
491
|
+
else if (seen.has(v)) return OVERHEAD_PER_KEY;
|
|
492
|
+
seen.add(v);
|
|
383
493
|
}
|
|
384
|
-
return
|
|
494
|
+
return estimateSize(v, seen);
|
|
495
|
+
};
|
|
496
|
+
if (Array.isArray(value)) {
|
|
497
|
+
let sum2 = OVERHEAD_PER_KEY * Math.max(value.length, 1);
|
|
498
|
+
for (const v of value) sum2 += visit(v);
|
|
499
|
+
return sum2;
|
|
385
500
|
}
|
|
386
|
-
|
|
501
|
+
if (value instanceof Map) {
|
|
502
|
+
let sum2 = OVERHEAD_PER_KEY * value.size;
|
|
503
|
+
for (const [k, v] of value) sum2 += estimateSize(k, seen) + visit(v);
|
|
504
|
+
return sum2;
|
|
505
|
+
}
|
|
506
|
+
if (value instanceof Set) {
|
|
507
|
+
let sum2 = OVERHEAD_PER_KEY * value.size;
|
|
508
|
+
for (const v of value) sum2 += visit(v);
|
|
509
|
+
return sum2;
|
|
510
|
+
}
|
|
511
|
+
if (ArrayBuffer.isView(value)) {
|
|
512
|
+
return value.byteLength + OVERHEAD_PER_KEY;
|
|
513
|
+
}
|
|
514
|
+
let sum = 0;
|
|
515
|
+
for (const [k, v] of Object.entries(value)) {
|
|
516
|
+
sum += k.length * 2 + OVERHEAD_PER_KEY + visit(v);
|
|
517
|
+
}
|
|
518
|
+
return sum;
|
|
387
519
|
};
|
|
388
520
|
|
|
389
521
|
// src/cache/hot-cache/ticker.ts
|
|
522
|
+
var MAX_TICK_INFLIGHT = 64;
|
|
390
523
|
var RefreshTicker = class {
|
|
391
524
|
constructor(host, tickIntervalMs) {
|
|
392
525
|
this.host = host;
|
|
@@ -421,17 +554,26 @@ var RefreshTicker = class {
|
|
|
421
554
|
this.timer = void 0;
|
|
422
555
|
}
|
|
423
556
|
}
|
|
424
|
-
/**
|
|
557
|
+
/**
|
|
558
|
+
* Background refresh pass: re-run loaders for due entries, swap fresh values.
|
|
559
|
+
* A per-tick in-flight budget bounds the stampede after downtime/throttling —
|
|
560
|
+
* one sweep can't launch unbounded simultaneous loaders across all queries;
|
|
561
|
+
* skipped entries stay due and are picked up by the next tick.
|
|
562
|
+
*/
|
|
425
563
|
tick() {
|
|
426
564
|
const now = Date.now();
|
|
565
|
+
let budget = MAX_TICK_INFLIGHT;
|
|
427
566
|
for (const q of this.host.queries().values()) {
|
|
428
567
|
if (q.refreshIntervalMs <= 0) continue;
|
|
429
568
|
for (const [key, entry] of [...q.lru.entries()]) {
|
|
430
569
|
if (entry.nextRefreshAt <= now) {
|
|
570
|
+
if (budget <= 0) return;
|
|
571
|
+
budget--;
|
|
572
|
+
entry.nextRefreshAt = now + q.refreshIntervalMs;
|
|
431
573
|
q.refreshes++;
|
|
432
574
|
void this.host.fetch(q, key, entry.args, q.gen).catch((err) => {
|
|
433
575
|
const current = q.lru.peek(key);
|
|
434
|
-
if (current) current.nextRefreshAt = now
|
|
576
|
+
if (current) current.nextRefreshAt = Date.now();
|
|
435
577
|
this.host.logger.warn?.(
|
|
436
578
|
{
|
|
437
579
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -465,8 +607,44 @@ var RefreshTicker = class {
|
|
|
465
607
|
|
|
466
608
|
// src/cache/hot-cache/types.ts
|
|
467
609
|
var WATCH_SEP = "\0";
|
|
610
|
+
var resolveWatchDb = (ref) => typeof ref.db === "function" ? ref.db() : ref.db;
|
|
611
|
+
var normalizeWatchId = (id) => {
|
|
612
|
+
if (typeof id === "string") return id;
|
|
613
|
+
if (typeof id === "number" || typeof id === "bigint") return String(id);
|
|
614
|
+
if (id instanceof Date) return `d${id.getTime()}`;
|
|
615
|
+
const hex = typeof id === "object" && id !== null ? id.toHexString : void 0;
|
|
616
|
+
if (typeof hex === "function") return hex.call(id);
|
|
617
|
+
return stableStringify(id);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// src/utils/watch-errors.ts
|
|
621
|
+
var isPermanentWatchError = (message) => (
|
|
622
|
+
// Standalone server: `$changeStream` is not supported at all.
|
|
623
|
+
/only supported on replica sets/i.test(message) || // Credentials/authorization problems: the user cannot run `aggregate` on
|
|
624
|
+
// this collection/server. Wrong creds, missing roles, or a user created
|
|
625
|
+
// without the `read`/`changeStream` privilege will never start working by
|
|
626
|
+
// retrying — fail once and stop instead of retrying forever.
|
|
627
|
+
/requires authentication|not authorized|unauthorized|authentication failed|Command .* requires authentication/i.test(
|
|
628
|
+
message
|
|
629
|
+
)
|
|
630
|
+
);
|
|
468
631
|
|
|
469
632
|
// src/cache/hot-cache/watcher.ts
|
|
633
|
+
var resolveDbSafe = (ref, logger) => {
|
|
634
|
+
try {
|
|
635
|
+
return resolveWatchDb(ref);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
logger.debug?.(
|
|
638
|
+
{ error: err instanceof Error ? err.message : String(err), collection: ref.collection },
|
|
639
|
+
"hot cache: watch db accessor not ready yet"
|
|
640
|
+
);
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
var documentKeyOf = (change) => {
|
|
645
|
+
const key = change?.documentKey;
|
|
646
|
+
return key ? key._id : void 0;
|
|
647
|
+
};
|
|
470
648
|
var WatchCoordinator = class {
|
|
471
649
|
constructor(host) {
|
|
472
650
|
this.host = host;
|
|
@@ -474,6 +652,8 @@ var WatchCoordinator = class {
|
|
|
474
652
|
streams = /* @__PURE__ */ new Map();
|
|
475
653
|
failedStreams = /* @__PURE__ */ new Set();
|
|
476
654
|
stopped = true;
|
|
655
|
+
/** Guards against stacking reconnect timers when several accessors lag. */
|
|
656
|
+
reopenTimer;
|
|
477
657
|
/** Number of currently open change streams. */
|
|
478
658
|
get size() {
|
|
479
659
|
return this.streams.size;
|
|
@@ -486,6 +666,7 @@ var WatchCoordinator = class {
|
|
|
486
666
|
/** Close every watcher and clear failure bookkeeping (idempotent). */
|
|
487
667
|
async stop() {
|
|
488
668
|
this.stopped = true;
|
|
669
|
+
if (this.reopenTimer) clearTimeout(this.reopenTimer);
|
|
489
670
|
const closes = [...this.streams.values()].map((stream) => {
|
|
490
671
|
try {
|
|
491
672
|
return Promise.resolve(stream.close()).catch(() => {
|
|
@@ -500,14 +681,32 @@ var WatchCoordinator = class {
|
|
|
500
681
|
}
|
|
501
682
|
openWatchStreams() {
|
|
502
683
|
const seen = /* @__PURE__ */ new Set();
|
|
684
|
+
let deferred = false;
|
|
503
685
|
for (const q of this.host.queries().values()) {
|
|
504
686
|
for (const ref of q.config.watch ?? []) {
|
|
505
|
-
const
|
|
687
|
+
const db = resolveDbSafe(ref, this.host.logger);
|
|
688
|
+
if (!db) {
|
|
689
|
+
deferred = true;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const key = `${db.databaseName}${WATCH_SEP}${ref.collection}`;
|
|
506
693
|
if (seen.has(key) || this.streams.has(key)) continue;
|
|
507
694
|
seen.add(key);
|
|
508
|
-
void this.watchLoop(key, ref)
|
|
695
|
+
void this.watchLoop(key, ref).catch((err) => {
|
|
696
|
+
this.host.logger.error?.(
|
|
697
|
+
{ error: err instanceof Error ? err.message : String(err), collection: ref.collection },
|
|
698
|
+
"hot cache: watcher crashed"
|
|
699
|
+
);
|
|
700
|
+
});
|
|
509
701
|
}
|
|
510
702
|
}
|
|
703
|
+
if (deferred && !this.stopped && !this.reopenTimer) {
|
|
704
|
+
this.reopenTimer = setTimeout(() => {
|
|
705
|
+
this.reopenTimer = void 0;
|
|
706
|
+
if (!this.stopped) this.openWatchStreams();
|
|
707
|
+
}, 5e3);
|
|
708
|
+
this.reopenTimer.unref?.();
|
|
709
|
+
}
|
|
511
710
|
}
|
|
512
711
|
/**
|
|
513
712
|
* Long-lived change-stream consumer for one collection. Each data change
|
|
@@ -521,11 +720,19 @@ var WatchCoordinator = class {
|
|
|
521
720
|
async watchLoop(key, ref) {
|
|
522
721
|
let backoff = 0;
|
|
523
722
|
while (!this.stopped) {
|
|
723
|
+
let connectedAt = Date.now();
|
|
524
724
|
try {
|
|
525
|
-
const
|
|
526
|
-
|
|
725
|
+
const db = resolveDbSafe(ref, this.host.logger);
|
|
726
|
+
if (!db) throw new Error("watch db accessor not available");
|
|
727
|
+
const stream = db.collection(ref.collection).watch([]);
|
|
728
|
+
stream.on("error", (err) => {
|
|
729
|
+
this.host.logger.debug?.(
|
|
730
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
731
|
+
"hot cache change stream error event"
|
|
732
|
+
);
|
|
527
733
|
});
|
|
528
734
|
this.streams.set(key, stream);
|
|
735
|
+
connectedAt = Date.now();
|
|
529
736
|
if (backoff > 0) {
|
|
530
737
|
this.host.logger.warn?.(
|
|
531
738
|
{ collection: ref.collection },
|
|
@@ -534,10 +741,13 @@ var WatchCoordinator = class {
|
|
|
534
741
|
this.host.invalidateCollection(ref.collection);
|
|
535
742
|
}
|
|
536
743
|
const iterator = stream[Symbol.asyncIterator]();
|
|
537
|
-
for await (const
|
|
538
|
-
|
|
744
|
+
for await (const change of iterator) {
|
|
745
|
+
const docId = documentKeyOf(change);
|
|
746
|
+
if (docId === void 0) this.host.invalidateCollection(ref.collection);
|
|
747
|
+
else this.host.invalidateDocument(ref.collection, docId);
|
|
539
748
|
}
|
|
540
749
|
} catch (err) {
|
|
750
|
+
if (Date.now() - connectedAt > 6e4) backoff = 0;
|
|
541
751
|
const message = err instanceof Error ? err.message : String(err);
|
|
542
752
|
this.host.logger.warn?.(
|
|
543
753
|
{ error: message, collection: ref.collection },
|
|
@@ -551,14 +761,14 @@ var WatchCoordinator = class {
|
|
|
551
761
|
} catch {
|
|
552
762
|
}
|
|
553
763
|
}
|
|
554
|
-
if (
|
|
764
|
+
if (isPermanentWatchError(message)) {
|
|
555
765
|
this.markStreamFailed(key);
|
|
556
766
|
return;
|
|
557
767
|
}
|
|
558
768
|
}
|
|
559
769
|
if (this.stopped) return;
|
|
560
770
|
backoff = Math.min(backoff + 1e3, 5e3);
|
|
561
|
-
await sleepJittered(backoff);
|
|
771
|
+
await sleepJittered(backoff, 1e3, true);
|
|
562
772
|
}
|
|
563
773
|
}
|
|
564
774
|
markStreamFailed(key) {
|
|
@@ -574,7 +784,9 @@ var WatchCoordinator = class {
|
|
|
574
784
|
const set = /* @__PURE__ */ new Set();
|
|
575
785
|
for (const q of this.host.queries().values()) {
|
|
576
786
|
for (const ref of q.config.watch ?? []) {
|
|
577
|
-
|
|
787
|
+
const db = resolveDbSafe(ref, this.host.logger);
|
|
788
|
+
if (!db) continue;
|
|
789
|
+
set.add(`${db.databaseName}${WATCH_SEP}${ref.collection}`);
|
|
578
790
|
}
|
|
579
791
|
}
|
|
580
792
|
return set;
|
|
@@ -615,10 +827,14 @@ var HotCache = class {
|
|
|
615
827
|
this.watcher = new WatchCoordinator({
|
|
616
828
|
queries: () => this.queries,
|
|
617
829
|
invalidateCollection: (collection) => this.invalidateCollection(collection),
|
|
830
|
+
invalidateDocument: (collection, id) => this.invalidateIds(collection, [id]),
|
|
618
831
|
isReplica: () => this._mode === "replica",
|
|
619
832
|
fallbackToStandalone: () => {
|
|
620
833
|
this._mode = "standalone";
|
|
621
|
-
if (this.refreshEnabled
|
|
834
|
+
if (this.refreshEnabled && !this.ticker.running) {
|
|
835
|
+
this.ticker.start();
|
|
836
|
+
this.ticker.warnUnboundedStaleness();
|
|
837
|
+
}
|
|
622
838
|
},
|
|
623
839
|
logger: this.logger
|
|
624
840
|
});
|
|
@@ -660,26 +876,27 @@ var HotCache = class {
|
|
|
660
876
|
ttlMs: config.ttlMs ?? this.options.defaultTtlMs ?? 0,
|
|
661
877
|
refreshIntervalMs: config.refreshIntervalMs ?? 0,
|
|
662
878
|
clone: config.clone ?? false,
|
|
663
|
-
maxValueBytes: config.maxValueBytes,
|
|
879
|
+
...config.maxValueBytes !== void 0 ? { maxValueBytes: config.maxValueBytes } : {},
|
|
880
|
+
hasIdWatch: (config.watch ?? []).some((ref) => typeof ref.idsOf === "function"),
|
|
664
881
|
gen: 0,
|
|
665
882
|
hits: 0,
|
|
666
883
|
misses: 0,
|
|
667
884
|
refreshes: 0,
|
|
668
885
|
loadErrors: 0,
|
|
669
886
|
sizeSkips: 0,
|
|
887
|
+
idDrops: 0,
|
|
670
888
|
warnedSize: false
|
|
671
889
|
};
|
|
672
890
|
this.queries.set(name, query);
|
|
673
891
|
if (this._mode === "replica" && !this.stopped) {
|
|
674
892
|
this.watcher.start();
|
|
675
893
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
else this.invalidateParams(name, ...args);
|
|
681
|
-
}
|
|
894
|
+
const get = (...args) => this.get(name, ...args);
|
|
895
|
+
const invalidate = (...args) => {
|
|
896
|
+
if (args.length === 0) this.invalidate(name);
|
|
897
|
+
else this.invalidateParams(name, ...args);
|
|
682
898
|
};
|
|
899
|
+
return { get, invalidate };
|
|
683
900
|
}
|
|
684
901
|
/**
|
|
685
902
|
* Read-through read by dynamic name lookup. Call parameters are hashed into
|
|
@@ -701,7 +918,7 @@ var HotCache = class {
|
|
|
701
918
|
q.lru.delete(key);
|
|
702
919
|
}
|
|
703
920
|
q.misses++;
|
|
704
|
-
return this.fetch(q, key, args, q.gen);
|
|
921
|
+
return await this.fetch(q, key, args, q.gen);
|
|
705
922
|
}
|
|
706
923
|
/** Drop every cached entry for a registered query. */
|
|
707
924
|
invalidate(name) {
|
|
@@ -734,6 +951,48 @@ var HotCache = class {
|
|
|
734
951
|
}
|
|
735
952
|
}
|
|
736
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* Fine-tuned purge: drop only the entries that depend on specific document
|
|
956
|
+
* ids of a watched collection. Two complementary mechanisms feed it:
|
|
957
|
+
*
|
|
958
|
+
* - **Replica mode** — the change stream extracts `documentKey._id` from
|
|
959
|
+
* each event and calls this with just that id, so a write to one document
|
|
960
|
+
* no longer purges every query bound to the whole collection.
|
|
961
|
+
* - **Manual (any mode)** — call it from ORM after-write hooks or anywhere
|
|
962
|
+
* else you know exactly which ids changed.
|
|
963
|
+
*
|
|
964
|
+
* Per query bound to `collection`, semantics are:
|
|
965
|
+
* - watch ref declares `idsOf` → only LRU entries whose extracted ids
|
|
966
|
+
* include one of `ids` are dropped (`depsAll` entries — where extraction
|
|
967
|
+
* failed — are dropped too; conservative). Sibling entries stay warm and
|
|
968
|
+
* `stats().perQuery[name].idDrops` counts what was dropped.
|
|
969
|
+
* - no `idsOf` on any bound ref → full purge of that query (the
|
|
970
|
+
* `invalidateCollection` behavior — the safe default).
|
|
971
|
+
*/
|
|
972
|
+
invalidateIds(collection, ids) {
|
|
973
|
+
if (ids.length === 0) return;
|
|
974
|
+
const changed = /* @__PURE__ */ new Set();
|
|
975
|
+
for (const id of ids) changed.add(`${collection}${WATCH_SEP}${normalizeWatchId(id)}`);
|
|
976
|
+
for (const q of this.queries.values()) {
|
|
977
|
+
const bound = q.config.watch?.filter((ref) => ref.collection === collection);
|
|
978
|
+
if (!bound || bound.length === 0) continue;
|
|
979
|
+
if (!bound.some((ref) => typeof ref.idsOf === "function")) {
|
|
980
|
+
q.lru.clear();
|
|
981
|
+
q.gen++;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
let dropped = 0;
|
|
985
|
+
for (const [key, entry] of [...q.lru.entries()]) {
|
|
986
|
+
const matches = entry.depsAll === true || entry.deps?.some((dep) => changed.has(dep)) === true;
|
|
987
|
+
if (matches) {
|
|
988
|
+
q.lru.delete(key);
|
|
989
|
+
dropped++;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
q.idDrops += dropped;
|
|
993
|
+
q.gen++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
737
996
|
/** Drop every cached entry for every query. */
|
|
738
997
|
clear() {
|
|
739
998
|
for (const q of this.queries.values()) {
|
|
@@ -743,9 +1002,10 @@ var HotCache = class {
|
|
|
743
1002
|
}
|
|
744
1003
|
/**
|
|
745
1004
|
* Runtime observability snapshot: total queries/entries plus per-query
|
|
746
|
-
* counters (`hits`, `misses`, background `refreshes`, `loadErrors`,
|
|
747
|
-
* `sizeSkips` for entries dropped by `maxValueBytes`
|
|
748
|
-
*
|
|
1005
|
+
* counters (`hits`, `misses`, background `refreshes`, `loadErrors`,
|
|
1006
|
+
* `sizeSkips` for entries dropped by `maxValueBytes`, and `idDrops` for
|
|
1007
|
+
* entries dropped by id-targeted invalidation). Useful for dashboards and
|
|
1008
|
+
* cache-effectiveness checks.
|
|
749
1009
|
*/
|
|
750
1010
|
stats() {
|
|
751
1011
|
const perQuery = {};
|
|
@@ -757,6 +1017,7 @@ var HotCache = class {
|
|
|
757
1017
|
refreshes: q.refreshes,
|
|
758
1018
|
loadErrors: q.loadErrors,
|
|
759
1019
|
sizeSkips: q.sizeSkips,
|
|
1020
|
+
idDrops: q.idDrops,
|
|
760
1021
|
evictions: q.lru.evictions
|
|
761
1022
|
};
|
|
762
1023
|
}
|
|
@@ -765,12 +1026,15 @@ var HotCache = class {
|
|
|
765
1026
|
/**
|
|
766
1027
|
* Probe replica support once, then start change-stream watchers (replica) or
|
|
767
1028
|
* the global refresh ticker (standalone). Idempotent — resolves the mode.
|
|
1029
|
+
* A FAILED start does not stick: the memoized promise is cleared so the next
|
|
1030
|
+
* `get()`/`start()` retries probing (one transient blip during startup must
|
|
1031
|
+
* not disable background freshness for the life of the instance).
|
|
768
1032
|
*/
|
|
769
1033
|
async start() {
|
|
770
1034
|
if (this.disposed) return this._mode;
|
|
771
1035
|
if (this.started) return this._mode;
|
|
772
1036
|
if (!this.startPromise) {
|
|
773
|
-
this.startPromise = this.runStart();
|
|
1037
|
+
this.startPromise = this.runStart().catch(this.forgetFailedStart);
|
|
774
1038
|
}
|
|
775
1039
|
return this.startPromise;
|
|
776
1040
|
}
|
|
@@ -792,6 +1056,17 @@ var HotCache = class {
|
|
|
792
1056
|
this._mode = "unknown";
|
|
793
1057
|
}
|
|
794
1058
|
/* ----------------------------- internals ----------------------------- */
|
|
1059
|
+
/**
|
|
1060
|
+
* A FAILED start does not stick: clear the memoized state so the next
|
|
1061
|
+
* `get()`/`start()` retries probing (one transient blip during startup must
|
|
1062
|
+
* not disable background freshness for the life of the instance).
|
|
1063
|
+
*/
|
|
1064
|
+
forgetFailedStart = (err) => {
|
|
1065
|
+
this.startPromise = void 0;
|
|
1066
|
+
this.startAttempted = false;
|
|
1067
|
+
this.started = false;
|
|
1068
|
+
throw err;
|
|
1069
|
+
};
|
|
795
1070
|
async runStart() {
|
|
796
1071
|
this.stopped = false;
|
|
797
1072
|
const supported = this.options.mode === "replica" ? true : this.options.mode === "standalone" ? false : await (this.options.probe ?? this.defaultProbe)();
|
|
@@ -815,20 +1090,34 @@ var HotCache = class {
|
|
|
815
1090
|
ensureStarted() {
|
|
816
1091
|
if (this.disposed || this.started || this.startAttempted) return;
|
|
817
1092
|
this.startAttempted = true;
|
|
818
|
-
|
|
1093
|
+
const origin = new Error("hot cache: background start() was triggered by this call");
|
|
1094
|
+
const reportStartFailure = (err) => {
|
|
1095
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
1096
|
+
if (wrapped.cause === void 0) wrapped.cause = origin;
|
|
819
1097
|
this.logger.warn?.(
|
|
820
|
-
{ error:
|
|
1098
|
+
{ error: wrapped.message, stack: wrapped.stack, trigger: origin.stack },
|
|
821
1099
|
"hot cache start failed; reads continue without auto-refresh"
|
|
822
1100
|
);
|
|
823
|
-
}
|
|
1101
|
+
};
|
|
1102
|
+
this.start().catch(reportStartFailure);
|
|
824
1103
|
}
|
|
825
1104
|
defaultProbe = async () => {
|
|
826
1105
|
for (const q of this.queries.values()) {
|
|
827
1106
|
for (const ref of q.config.watch ?? []) {
|
|
828
|
-
|
|
1107
|
+
let db;
|
|
1108
|
+
try {
|
|
1109
|
+
db = resolveWatchDb(ref);
|
|
1110
|
+
} catch {
|
|
1111
|
+
this.logger.warn?.(
|
|
1112
|
+
{},
|
|
1113
|
+
"hot cache: replica probe could not resolve the watch db (accessor threw) \u2014 falling back to the standalone ticker; pass `mode` or a custom `probe` to pin the behavior"
|
|
1114
|
+
);
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
const caps = await probeMongoCapabilities(db);
|
|
829
1118
|
if (!caps.probed) {
|
|
830
1119
|
this.logger.warn?.(
|
|
831
|
-
{ db:
|
|
1120
|
+
{ db: db.databaseName },
|
|
832
1121
|
"hot cache: replica probe returned no result (timeout?) \u2014 falling back to the standalone ticker; pass `mode` or a custom `probe` to pin the behavior"
|
|
833
1122
|
);
|
|
834
1123
|
return false;
|
|
@@ -839,17 +1128,18 @@ var HotCache = class {
|
|
|
839
1128
|
return false;
|
|
840
1129
|
};
|
|
841
1130
|
fetch(q, key, args, gen) {
|
|
842
|
-
const dedupeKey = `${q.name}${WATCH_SEP}${key}`;
|
|
843
|
-
|
|
1131
|
+
const dedupeKey = `${q.name}${WATCH_SEP}g${gen}${WATCH_SEP}${key}`;
|
|
1132
|
+
const loadOnce = async () => {
|
|
844
1133
|
try {
|
|
845
1134
|
const value = await q.config.loader(...args);
|
|
846
1135
|
if (q.gen === gen) {
|
|
847
|
-
|
|
1136
|
+
const bytes = q.maxValueBytes !== void 0 ? estimateSize(value) : void 0;
|
|
1137
|
+
if (bytes !== void 0 && bytes > q.maxValueBytes) {
|
|
848
1138
|
q.sizeSkips++;
|
|
849
1139
|
if (!q.warnedSize) {
|
|
850
1140
|
q.warnedSize = true;
|
|
851
1141
|
this.logger.warn?.(
|
|
852
|
-
{ query: q.name, bytes
|
|
1142
|
+
{ query: q.name, bytes, max: q.maxValueBytes },
|
|
853
1143
|
"hot cache: value exceeds maxValueBytes \u2014 returned but not cached"
|
|
854
1144
|
);
|
|
855
1145
|
}
|
|
@@ -862,17 +1152,42 @@ var HotCache = class {
|
|
|
862
1152
|
q.loadErrors++;
|
|
863
1153
|
throw err;
|
|
864
1154
|
}
|
|
865
|
-
}
|
|
1155
|
+
};
|
|
1156
|
+
return this.inflight.run(dedupeKey, loadOnce);
|
|
866
1157
|
}
|
|
867
1158
|
setEntry(q, key, args, value) {
|
|
868
1159
|
const now = Date.now();
|
|
1160
|
+
const deps2 = q.hasIdWatch ? this.computeDeps(q, args) : {};
|
|
869
1161
|
q.lru.set(key, {
|
|
870
1162
|
value,
|
|
871
1163
|
args,
|
|
872
1164
|
expiresAt: q.ttlMs > 0 ? now + q.ttlMs : Infinity,
|
|
873
|
-
nextRefreshAt: q.refreshIntervalMs > 0 ? now + q.refreshIntervalMs : Infinity
|
|
1165
|
+
nextRefreshAt: q.refreshIntervalMs > 0 ? now + q.refreshIntervalMs : Infinity,
|
|
1166
|
+
...deps2
|
|
874
1167
|
});
|
|
875
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Extract the watched-document dependencies of one loader call. Namespaced by
|
|
1171
|
+
* collection so multi-collection watchers never cross-invalidate. An extractor
|
|
1172
|
+
* that throws or returns a non-array yields `{ depsAll: true }` — the entry is
|
|
1173
|
+
* then dropped by ANY targeted change on its watched collections instead of
|
|
1174
|
+
* being incorrectly kept.
|
|
1175
|
+
*/
|
|
1176
|
+
computeDeps(q, args) {
|
|
1177
|
+
const deps2 = [];
|
|
1178
|
+
for (const ref of q.config.watch ?? []) {
|
|
1179
|
+
if (!ref.idsOf) continue;
|
|
1180
|
+
const extract = ref.idsOf;
|
|
1181
|
+
try {
|
|
1182
|
+
const ids = extract(...args);
|
|
1183
|
+
if (!Array.isArray(ids)) return { depsAll: true };
|
|
1184
|
+
for (const id of ids) deps2.push(`${ref.collection}${WATCH_SEP}${normalizeWatchId(id)}`);
|
|
1185
|
+
} catch {
|
|
1186
|
+
return { depsAll: true };
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return { deps: deps2 };
|
|
1190
|
+
}
|
|
876
1191
|
};
|
|
877
1192
|
var createHotCache = (options) => new HotCache(options);
|
|
878
1193
|
|
|
@@ -883,6 +1198,12 @@ var cacheCollectionKey = (dbName, physical) => `${dbName}${NS_SEP}${physical}`;
|
|
|
883
1198
|
var QueryCache = class {
|
|
884
1199
|
lru;
|
|
885
1200
|
index = /* @__PURE__ */ new Map();
|
|
1201
|
+
/**
|
|
1202
|
+
* Reverse index: key → every collection it is registered under. Without it,
|
|
1203
|
+
* keys dropped by TTL expiry / capacity eviction / version-guard would stay
|
|
1204
|
+
* listed in `index` forever (unbounded growth + O(dead) invalidation scans).
|
|
1205
|
+
*/
|
|
1206
|
+
keyCollections = /* @__PURE__ */ new Map();
|
|
886
1207
|
/** Per-collection version — bumped on every invalidation/clear of that collection. */
|
|
887
1208
|
colVersions = /* @__PURE__ */ new Map();
|
|
888
1209
|
ttlMs;
|
|
@@ -894,7 +1215,10 @@ var QueryCache = class {
|
|
|
894
1215
|
invalidateEvents = 0;
|
|
895
1216
|
clearEvents = 0;
|
|
896
1217
|
constructor(options = {}) {
|
|
897
|
-
this.lru = new LRU({
|
|
1218
|
+
this.lru = new LRU({
|
|
1219
|
+
max: options.maxSize ?? 500,
|
|
1220
|
+
onEvict: (key) => this.unregisterKey(key)
|
|
1221
|
+
});
|
|
898
1222
|
this.ttlMs = options.ttlMs ?? 0;
|
|
899
1223
|
this.clone = options.clone ?? false;
|
|
900
1224
|
}
|
|
@@ -934,14 +1258,14 @@ var QueryCache = class {
|
|
|
934
1258
|
return void 0;
|
|
935
1259
|
}
|
|
936
1260
|
if (this.ttlMs > 0 && Date.now() > entry.expiresAt) {
|
|
937
|
-
this.
|
|
1261
|
+
this.dropKey(key);
|
|
938
1262
|
this.misses++;
|
|
939
1263
|
return void 0;
|
|
940
1264
|
}
|
|
941
1265
|
if (entry.versions) {
|
|
942
1266
|
for (const [col, ver] of Object.entries(entry.versions)) {
|
|
943
1267
|
if (this.versionOf(col) !== ver) {
|
|
944
|
-
this.
|
|
1268
|
+
this.dropKey(key);
|
|
945
1269
|
this.misses++;
|
|
946
1270
|
return void 0;
|
|
947
1271
|
}
|
|
@@ -950,14 +1274,36 @@ var QueryCache = class {
|
|
|
950
1274
|
this.hits++;
|
|
951
1275
|
return this.clone ? cloneDeep(entry.value) : entry.value;
|
|
952
1276
|
}
|
|
1277
|
+
/** Remove a key from the LRU and every collection index that references it. */
|
|
1278
|
+
dropKey(key) {
|
|
1279
|
+
this.lru.delete(key);
|
|
1280
|
+
this.unregisterKey(key);
|
|
1281
|
+
}
|
|
1282
|
+
/** Remove `key` from `index` (called for evictions AND explicit drops). */
|
|
1283
|
+
unregisterKey(key) {
|
|
1284
|
+
const collections = this.keyCollections.get(key);
|
|
1285
|
+
if (!collections) return;
|
|
1286
|
+
this.keyCollections.delete(key);
|
|
1287
|
+
for (const collection of collections) {
|
|
1288
|
+
const keys = this.index.get(collection);
|
|
1289
|
+
if (!keys) continue;
|
|
1290
|
+
keys.delete(key);
|
|
1291
|
+
if (keys.size === 0) this.index.delete(collection);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
953
1294
|
set(key, value, ttlMs, collections, versions) {
|
|
954
1295
|
this.sets++;
|
|
955
1296
|
const ttl = ttlMs ?? this.ttlMs;
|
|
956
1297
|
const expiresAt = ttl > 0 ? Date.now() + ttl : Infinity;
|
|
957
|
-
this.lru.set(key, {
|
|
1298
|
+
this.lru.set(key, {
|
|
1299
|
+
value,
|
|
1300
|
+
expiresAt,
|
|
1301
|
+
...versions !== void 0 ? { versions } : {}
|
|
1302
|
+
});
|
|
958
1303
|
const sep = key.indexOf(SEP);
|
|
959
1304
|
const primary = sep === -1 ? key : key.slice(0, sep);
|
|
960
1305
|
const collectionKeys = /* @__PURE__ */ new Set([primary, ...collections ?? []]);
|
|
1306
|
+
this.keyCollections.set(key, collectionKeys);
|
|
961
1307
|
for (const collection of collectionKeys) {
|
|
962
1308
|
let keys = this.index.get(collection);
|
|
963
1309
|
if (!keys) {
|
|
@@ -969,7 +1315,7 @@ var QueryCache = class {
|
|
|
969
1315
|
}
|
|
970
1316
|
delete(key) {
|
|
971
1317
|
this.deletes++;
|
|
972
|
-
this.
|
|
1318
|
+
this.dropKey(key);
|
|
973
1319
|
}
|
|
974
1320
|
/** Drop every cached entry belonging to a physical collection. */
|
|
975
1321
|
invalidateByCollection(collection) {
|
|
@@ -977,7 +1323,7 @@ var QueryCache = class {
|
|
|
977
1323
|
const keys = this.index.get(collection);
|
|
978
1324
|
if (!keys) return;
|
|
979
1325
|
this.invalidateEvents++;
|
|
980
|
-
for (const key of keys) this.
|
|
1326
|
+
for (const key of [...keys]) this.dropKey(key);
|
|
981
1327
|
this.index.delete(collection);
|
|
982
1328
|
}
|
|
983
1329
|
clear() {
|
|
@@ -987,19 +1333,35 @@ var QueryCache = class {
|
|
|
987
1333
|
}
|
|
988
1334
|
this.lru.clear();
|
|
989
1335
|
this.index.clear();
|
|
1336
|
+
this.keyCollections.clear();
|
|
990
1337
|
}
|
|
991
1338
|
};
|
|
992
1339
|
|
|
993
1340
|
// src/graceful-transaction.ts
|
|
994
1341
|
var withGracefulMongoTransaction = async (runner, fn, options = {}) => {
|
|
995
|
-
|
|
1342
|
+
let enabled = true;
|
|
1343
|
+
if (options.capabilities !== void 0) {
|
|
1344
|
+
const env = readMongoTransactionsEnvOverride();
|
|
1345
|
+
if (env !== void 0) {
|
|
1346
|
+
enabled = env;
|
|
1347
|
+
} else {
|
|
1348
|
+
const state = options.capabilities.get();
|
|
1349
|
+
if (state.probed) {
|
|
1350
|
+
enabled = state.transactionsSupported;
|
|
1351
|
+
} else if (options.urlHint) {
|
|
1352
|
+
enabled = /replicaSet=|mongodb\+srv:/i.test(options.urlHint);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
996
1356
|
if (!enabled) return fn(null);
|
|
997
1357
|
try {
|
|
998
1358
|
return await runner.transaction(fn);
|
|
999
1359
|
} catch (err) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1360
|
+
if (isTransactionUnsupportedError(err)) {
|
|
1361
|
+
options.logger?.warn?.(
|
|
1362
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
1363
|
+
"transactions unsupported, falling back"
|
|
1364
|
+
);
|
|
1003
1365
|
return fn(null);
|
|
1004
1366
|
}
|
|
1005
1367
|
if (options.wrapMongoErrors !== false) {
|
|
@@ -1098,13 +1460,17 @@ var DataLoader = class {
|
|
|
1098
1460
|
}
|
|
1099
1461
|
}
|
|
1100
1462
|
async flush() {
|
|
1101
|
-
if (this.flushing)
|
|
1463
|
+
if (this.flushing) {
|
|
1464
|
+
this.scheduled = false;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1102
1467
|
if (this.queue.length === 0) {
|
|
1103
1468
|
this.scheduled = false;
|
|
1104
1469
|
return;
|
|
1105
1470
|
}
|
|
1106
1471
|
const items = this.queue.splice(0, this.maxBatchSize);
|
|
1107
1472
|
this.flushing = true;
|
|
1473
|
+
this.scheduled = false;
|
|
1108
1474
|
try {
|
|
1109
1475
|
const resultMap = await this.batchFn(items.map((item) => item.key));
|
|
1110
1476
|
for (const item of items) {
|
|
@@ -1119,12 +1485,8 @@ var DataLoader = class {
|
|
|
1119
1485
|
}
|
|
1120
1486
|
} finally {
|
|
1121
1487
|
this.flushing = false;
|
|
1122
|
-
this.scheduled
|
|
1123
|
-
|
|
1124
|
-
this.scheduled = true;
|
|
1125
|
-
queueMicrotask(() => {
|
|
1126
|
-
void this.flush();
|
|
1127
|
-
});
|
|
1488
|
+
if (!this.scheduled && this.queue.length > 0) {
|
|
1489
|
+
this.scheduleFlush();
|
|
1128
1490
|
}
|
|
1129
1491
|
}
|
|
1130
1492
|
}
|
|
@@ -1135,6 +1497,7 @@ var canonicalKey = (value) => {
|
|
|
1135
1497
|
const anyValue = value;
|
|
1136
1498
|
if (typeof anyValue.toHexString === "function") return `oid:${anyValue.toHexString()}`;
|
|
1137
1499
|
if (value instanceof Date) return `date:${value.getTime()}`;
|
|
1500
|
+
return `obj:${stableStringify(value)}`;
|
|
1138
1501
|
}
|
|
1139
1502
|
return `${typeof value}:${String(value)}`;
|
|
1140
1503
|
};
|
|
@@ -1145,13 +1508,14 @@ var JOURNAL_COLLECTION = "_migrations";
|
|
|
1145
1508
|
|
|
1146
1509
|
// src/migrations/files.ts
|
|
1147
1510
|
var migrationNameOf = (file) => basename(file).replace(/\.(ts|js|mjs)$/, "");
|
|
1511
|
+
var numericPrefixOf = (file) => Number(MIGRATION_FILE.exec(basename(file))?.[1] ?? Number.MAX_SAFE_INTEGER);
|
|
1148
1512
|
var listMigrationFiles = async (dir) => {
|
|
1149
1513
|
const files = await readdir(dir);
|
|
1150
|
-
return files.filter((file) => MIGRATION_FILE.test(file)).sort();
|
|
1514
|
+
return files.filter((file) => MIGRATION_FILE.test(file)).sort((a, b) => numericPrefixOf(a) - numericPrefixOf(b) || a.localeCompare(b));
|
|
1151
1515
|
};
|
|
1152
1516
|
var loadMigration = async (dir, file) => {
|
|
1153
1517
|
const mod = await import(pathToFileURL(join(dir, file)).href);
|
|
1154
|
-
return { name: migrationNameOf(file)
|
|
1518
|
+
return { ...mod, name: migrationNameOf(file) };
|
|
1155
1519
|
};
|
|
1156
1520
|
var ensureMigrationDir = async (dir) => {
|
|
1157
1521
|
await mkdir(dir, { recursive: true });
|
|
@@ -1165,45 +1529,78 @@ var nextMigrationNumber = async (dir) => {
|
|
|
1165
1529
|
};
|
|
1166
1530
|
|
|
1167
1531
|
// src/migrations/journal.ts
|
|
1532
|
+
var isDuplicateKeyError = (err) => {
|
|
1533
|
+
const code = err?.code;
|
|
1534
|
+
if (code === 11e3 || code === 11001) return true;
|
|
1535
|
+
return /E11000 duplicate key/.test(String(err?.message ?? ""));
|
|
1536
|
+
};
|
|
1168
1537
|
var createMigrationJournal = (manager) => {
|
|
1538
|
+
let indexReady = false;
|
|
1539
|
+
const ensureIndex = async () => {
|
|
1540
|
+
if (indexReady) return;
|
|
1541
|
+
try {
|
|
1542
|
+
await manager.createIndex?.(JOURNAL_COLLECTION, { name: 1 }, { unique: true });
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
indexReady = true;
|
|
1546
|
+
};
|
|
1169
1547
|
const appliedNames = async () => {
|
|
1170
1548
|
const rows = await manager.findMany(JOURNAL_COLLECTION);
|
|
1171
1549
|
return new Set(
|
|
1172
1550
|
rows.filter((row) => row.status === void 0 || row.status === "applied").map((row) => row.name)
|
|
1173
1551
|
);
|
|
1174
1552
|
};
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
+
const claim = async (name, file, leaseMs) => {
|
|
1563
|
+
await ensureIndex();
|
|
1564
|
+
const now = /* @__PURE__ */ new Date();
|
|
1565
|
+
const expiresAt = new Date(Date.now() + leaseMs);
|
|
1566
|
+
const stole = await manager.findOneAndUpdate(
|
|
1567
|
+
JOURNAL_COLLECTION,
|
|
1568
|
+
stealableFilter(name, now, new Date(Date.now() - leaseMs)),
|
|
1569
|
+
{ $set: { status: "running", appliedAt: now, file, expiresAt } },
|
|
1570
|
+
{ returnDocument: "before" }
|
|
1571
|
+
);
|
|
1572
|
+
if (stole !== null) return true;
|
|
1573
|
+
try {
|
|
1574
|
+
await manager.insertOne(JOURNAL_COLLECTION, {
|
|
1575
|
+
name,
|
|
1576
|
+
appliedAt: now,
|
|
1577
|
+
status: "running",
|
|
1578
|
+
file,
|
|
1579
|
+
expiresAt
|
|
1580
|
+
});
|
|
1581
|
+
return true;
|
|
1582
|
+
} catch (err) {
|
|
1583
|
+
if (isDuplicateKeyError(err)) return false;
|
|
1584
|
+
throw err;
|
|
1585
|
+
}
|
|
1181
1586
|
};
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1587
|
+
const renew = async (name, leaseMs) => {
|
|
1588
|
+
await manager.updateOne(
|
|
1184
1589
|
JOURNAL_COLLECTION,
|
|
1185
|
-
{ name },
|
|
1186
|
-
{
|
|
1187
|
-
$setOnInsert: {
|
|
1188
|
-
name,
|
|
1189
|
-
appliedAt: /* @__PURE__ */ new Date(),
|
|
1190
|
-
status: "running",
|
|
1191
|
-
file
|
|
1192
|
-
}
|
|
1193
|
-
},
|
|
1194
|
-
{ upsert: true, returnDocument: "before" }
|
|
1590
|
+
{ name, status: "running" },
|
|
1591
|
+
{ $set: { expiresAt: new Date(Date.now() + leaseMs) } }
|
|
1195
1592
|
);
|
|
1196
|
-
return claimed === null;
|
|
1197
1593
|
};
|
|
1198
1594
|
const markApplied = async (name) => {
|
|
1199
|
-
await manager.updateOne(
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1595
|
+
const res = await manager.updateOne(
|
|
1596
|
+
JOURNAL_COLLECTION,
|
|
1597
|
+
{ name, status: "running" },
|
|
1598
|
+
{ $set: { status: "applied" } }
|
|
1599
|
+
);
|
|
1600
|
+
if (res?.matchedCount === 0) {
|
|
1601
|
+
await manager.insertOne(JOURNAL_COLLECTION, { name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }).catch(() => {
|
|
1205
1602
|
});
|
|
1206
|
-
}
|
|
1603
|
+
}
|
|
1207
1604
|
};
|
|
1208
1605
|
const removeRunning = async (name) => {
|
|
1209
1606
|
await manager.deleteOne(JOURNAL_COLLECTION, { name, status: "running" }).catch(() => {
|
|
@@ -1212,45 +1609,76 @@ var createMigrationJournal = (manager) => {
|
|
|
1212
1609
|
const unrecord = async (name) => {
|
|
1213
1610
|
await manager.deleteOne(JOURNAL_COLLECTION, { name });
|
|
1214
1611
|
};
|
|
1215
|
-
return { appliedNames,
|
|
1612
|
+
return { appliedNames, claim, renew, markApplied, removeRunning, unrecord };
|
|
1216
1613
|
};
|
|
1217
1614
|
|
|
1218
1615
|
// src/migrations/index.ts
|
|
1616
|
+
var DEFAULT_LEASE_MS = 12e4;
|
|
1219
1617
|
var createMongoMigrationRunner = (service, options = {}) => {
|
|
1220
1618
|
const migrationDir = options.migrationDir ?? service.config?.migrationDir ?? "./migrations";
|
|
1221
1619
|
const logger = options.logger ?? service.logger;
|
|
1620
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
1222
1621
|
const manager = () => {
|
|
1223
|
-
const
|
|
1622
|
+
const keys = Object.keys(service.db);
|
|
1623
|
+
const key = options.db !== void 0 ? options.db : keys[0];
|
|
1224
1624
|
const db = key ? service.db[key] : void 0;
|
|
1225
|
-
if (!db)
|
|
1625
|
+
if (!db) {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
options.db !== void 0 && !keys.includes(options.db) ? `Unknown database "${options.db}" \u2014 connected: ${keys.join(", ")}` : "No database client connected \u2014 call service.makeConnections() first"
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1226
1630
|
return db;
|
|
1227
1631
|
};
|
|
1632
|
+
const startHeartbeat = (journal, name) => {
|
|
1633
|
+
const timer = setInterval(
|
|
1634
|
+
() => {
|
|
1635
|
+
void journal.renew(name, leaseMs).catch(() => {
|
|
1636
|
+
});
|
|
1637
|
+
},
|
|
1638
|
+
Math.max(leaseMs / 2, 1e3)
|
|
1639
|
+
);
|
|
1640
|
+
timer.unref?.();
|
|
1641
|
+
return () => clearInterval(timer);
|
|
1642
|
+
};
|
|
1228
1643
|
const up = async () => {
|
|
1229
1644
|
await ensureMigrationDir(migrationDir);
|
|
1230
1645
|
const journal = createMigrationJournal(manager());
|
|
1231
|
-
await journal.cleanupStale();
|
|
1232
1646
|
const files = await listMigrationFiles(migrationDir);
|
|
1233
1647
|
const applied = await journal.appliedNames();
|
|
1234
1648
|
const pending = files.filter((file) => !applied.has(migrationNameOf(file)));
|
|
1235
1649
|
for (const file of pending) {
|
|
1236
1650
|
const migration = await loadMigration(migrationDir, file);
|
|
1237
1651
|
logger.info({ name: migration.name }, "running migration up");
|
|
1238
|
-
if (!await journal.claim(migration.name, basename(file))) {
|
|
1239
|
-
logger.info(
|
|
1652
|
+
if (!await journal.claim(migration.name, basename(file), leaseMs)) {
|
|
1653
|
+
logger.info(
|
|
1654
|
+
{ name: migration.name },
|
|
1655
|
+
"migration claimed by another runner or already applied, skipping"
|
|
1656
|
+
);
|
|
1240
1657
|
continue;
|
|
1241
1658
|
}
|
|
1659
|
+
const stopHeartbeat = startHeartbeat(journal, migration.name);
|
|
1242
1660
|
try {
|
|
1243
1661
|
await migration.up({ service, logger, name: migration.name });
|
|
1244
1662
|
await journal.markApplied(migration.name);
|
|
1245
1663
|
} catch (err) {
|
|
1246
1664
|
await journal.removeRunning(migration.name);
|
|
1247
1665
|
throw err;
|
|
1666
|
+
} finally {
|
|
1667
|
+
stopHeartbeat();
|
|
1248
1668
|
}
|
|
1249
1669
|
logger.info({ name: migration.name }, "migration applied");
|
|
1250
1670
|
}
|
|
1251
1671
|
};
|
|
1252
1672
|
const down = async (targetName) => {
|
|
1253
1673
|
const files = await listMigrationFiles(migrationDir);
|
|
1674
|
+
if (targetName !== void 0) {
|
|
1675
|
+
const known = files.some((file) => migrationNameOf(file) === targetName);
|
|
1676
|
+
if (!known) {
|
|
1677
|
+
throw new Error(
|
|
1678
|
+
`down(): unknown migration "${targetName}" \u2014 known: ${files.map(migrationNameOf).join(", ") || "(none)"}`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1254
1682
|
const journal = createMigrationJournal(manager());
|
|
1255
1683
|
const applied = await journal.appliedNames();
|
|
1256
1684
|
const appliedFiles = files.filter((file) => applied.has(migrationNameOf(file))).reverse();
|
|
@@ -1260,7 +1688,7 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1260
1688
|
await migration.down({ service, logger, name: migration.name });
|
|
1261
1689
|
await journal.unrecord(migration.name);
|
|
1262
1690
|
logger.info({ name: migration.name }, "migration rolled back");
|
|
1263
|
-
if (targetName && migration.name === targetName) break;
|
|
1691
|
+
if (targetName !== void 0 && migration.name === targetName) break;
|
|
1264
1692
|
}
|
|
1265
1693
|
};
|
|
1266
1694
|
const status = async () => {
|
|
@@ -1274,10 +1702,6 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1274
1702
|
};
|
|
1275
1703
|
const create = async (name) => {
|
|
1276
1704
|
await ensureMigrationDir(migrationDir);
|
|
1277
|
-
const next = await nextMigrationNumber(migrationDir);
|
|
1278
|
-
const padded = String(next).padStart(3, "0");
|
|
1279
|
-
const fileName = `${padded}_${name}.ts`;
|
|
1280
|
-
const filePath = join(migrationDir, fileName);
|
|
1281
1705
|
const template = `import type { MigrationContext } from '@ignex/ninox';
|
|
1282
1706
|
|
|
1283
1707
|
export const up = async (ctx: MigrationContext): Promise<void> => {
|
|
@@ -1290,15 +1714,26 @@ export const down = async (ctx: MigrationContext): Promise<void> => {
|
|
|
1290
1714
|
// await users.client.dropCollection('users');
|
|
1291
1715
|
};
|
|
1292
1716
|
`;
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1717
|
+
for (; ; ) {
|
|
1718
|
+
const next = await nextMigrationNumber(migrationDir);
|
|
1719
|
+
const padded = String(next).padStart(3, "0");
|
|
1720
|
+
const fileName = `${padded}_${name}.ts`;
|
|
1721
|
+
const filePath = join(migrationDir, fileName);
|
|
1722
|
+
try {
|
|
1723
|
+
await writeFile(filePath, template, { encoding: "utf8", flag: "wx" });
|
|
1724
|
+
logger.info({ name, file: fileName }, "migration scaffolded");
|
|
1725
|
+
return filePath;
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
if (err?.code === "EEXIST") continue;
|
|
1728
|
+
throw err;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1296
1731
|
};
|
|
1297
1732
|
return { up, down, status, create };
|
|
1298
1733
|
};
|
|
1299
1734
|
|
|
1300
1735
|
// src/mongo-helpers.ts
|
|
1301
|
-
var withRetry = async (fn, options = {}
|
|
1736
|
+
var withRetry = async (fn, options = {}) => {
|
|
1302
1737
|
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
1303
1738
|
const delayMs = options.delayMs ?? 150;
|
|
1304
1739
|
const logger = options.logger;
|
|
@@ -1311,7 +1746,8 @@ var withRetry = async (fn, options = {}, _ctx) => {
|
|
|
1311
1746
|
lastError = err;
|
|
1312
1747
|
attempt += 1;
|
|
1313
1748
|
if (attempt >= maxAttempts || !isMongoTransientError(err)) throw err;
|
|
1314
|
-
const
|
|
1749
|
+
const base = delayMs * 2 ** (attempt - 1);
|
|
1750
|
+
const backoff = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
1315
1751
|
logger?.debug?.({ attempt, backoffMs: backoff, error: err?.message }, "retrying");
|
|
1316
1752
|
await sleep(backoff);
|
|
1317
1753
|
}
|
|
@@ -1341,7 +1777,13 @@ var DEFAULT_FIND_LIMIT = 100;
|
|
|
1341
1777
|
var MAX_BATCH_OPS = 1e5;
|
|
1342
1778
|
|
|
1343
1779
|
// src/query-builder/query-builder.ts
|
|
1344
|
-
var
|
|
1780
|
+
var mergeFilterKey = (prev, next) => {
|
|
1781
|
+
if (prev !== null && typeof prev === "object" && !Array.isArray(prev) && next !== null && typeof next === "object" && !Array.isArray(next)) {
|
|
1782
|
+
return { ...prev, ...next };
|
|
1783
|
+
}
|
|
1784
|
+
return next;
|
|
1785
|
+
};
|
|
1786
|
+
var QueryBuilder = class _QueryBuilder {
|
|
1345
1787
|
constructor(ctx) {
|
|
1346
1788
|
this.ctx = ctx;
|
|
1347
1789
|
}
|
|
@@ -1354,77 +1796,112 @@ var QueryBuilder = class {
|
|
|
1354
1796
|
get filter() {
|
|
1355
1797
|
return this._filter;
|
|
1356
1798
|
}
|
|
1357
|
-
/**
|
|
1799
|
+
/** Copy-on-write fork used by every chain method. */
|
|
1800
|
+
fork() {
|
|
1801
|
+
const next = new _QueryBuilder(this.ctx);
|
|
1802
|
+
next._filter = this._filter;
|
|
1803
|
+
if (this._sort !== void 0) next._sort = this._sort;
|
|
1804
|
+
if (this._skip !== void 0) next._skip = this._skip;
|
|
1805
|
+
if (this._limit !== void 0) next._limit = this._limit;
|
|
1806
|
+
if (this._projection !== void 0) next._projection = this._projection;
|
|
1807
|
+
next._options = { ...this._options };
|
|
1808
|
+
return next;
|
|
1809
|
+
}
|
|
1810
|
+
/** Merge a filter into the current one (operator docs deep-merge per key). */
|
|
1358
1811
|
where(filter) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1812
|
+
const next = this.fork();
|
|
1813
|
+
const merged = { ...next._filter };
|
|
1814
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1815
|
+
merged[key] = key in merged ? mergeFilterKey(merged[key], value) : value;
|
|
1816
|
+
}
|
|
1817
|
+
next._filter = merged;
|
|
1818
|
+
return next;
|
|
1361
1819
|
}
|
|
1362
|
-
/** AND-combine the current filter with a new one. */
|
|
1820
|
+
/** AND-combine the current filter with a new one (stays flat across repeats). */
|
|
1363
1821
|
and(filter) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1822
|
+
const next = this.fork();
|
|
1823
|
+
const current = next._filter;
|
|
1824
|
+
const existing = Array.isArray(current.$and) ? current.$and : Object.keys(current).length > 0 ? [current] : [];
|
|
1825
|
+
next._filter = { $and: [...existing, filter] };
|
|
1826
|
+
return next;
|
|
1366
1827
|
}
|
|
1367
|
-
/**
|
|
1828
|
+
/**
|
|
1829
|
+
* OR-combine: current filter AND (filter1 OR filter2 ...). Requires at least
|
|
1830
|
+
* one filter — an empty `$or` is rejected by the server.
|
|
1831
|
+
*/
|
|
1368
1832
|
or(...filters) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1833
|
+
if (filters.length === 0) {
|
|
1834
|
+
throw new BadRequest("QueryBuilder.or() requires at least one filter");
|
|
1835
|
+
}
|
|
1836
|
+
return this.and(filters.length === 1 ? filters[0] : { $or: filters });
|
|
1371
1837
|
}
|
|
1372
1838
|
/** Set the sort order (`{ field: 1 | -1 }`). */
|
|
1373
1839
|
sort(sort) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1840
|
+
const next = this.fork();
|
|
1841
|
+
next._sort = sort;
|
|
1842
|
+
return next;
|
|
1376
1843
|
}
|
|
1377
1844
|
/** Skip the first `n` matching documents. */
|
|
1378
1845
|
skip(n) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1846
|
+
const next = this.fork();
|
|
1847
|
+
next._skip = n;
|
|
1848
|
+
return next;
|
|
1381
1849
|
}
|
|
1382
1850
|
/** Limit the result set to `n` documents. */
|
|
1383
1851
|
limit(n) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1852
|
+
const next = this.fork();
|
|
1853
|
+
next._limit = n;
|
|
1854
|
+
return next;
|
|
1386
1855
|
}
|
|
1387
1856
|
/** Project only the given fields — the projection is pushed to the driver. */
|
|
1388
1857
|
select(fields) {
|
|
1858
|
+
const next = this.fork();
|
|
1389
1859
|
const projection = {};
|
|
1390
1860
|
for (const field of fields) projection[field] = 1;
|
|
1391
|
-
|
|
1392
|
-
return
|
|
1861
|
+
next._projection = projection;
|
|
1862
|
+
return next;
|
|
1393
1863
|
}
|
|
1394
1864
|
/** Raw projection document (`{ field: 1 }` / `{ field: 0 }`). */
|
|
1395
1865
|
project(projection) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1866
|
+
const next = this.fork();
|
|
1867
|
+
next._projection = projection;
|
|
1868
|
+
return next;
|
|
1398
1869
|
}
|
|
1399
1870
|
/** Set the index hint. */
|
|
1400
1871
|
hint(hint) {
|
|
1401
|
-
|
|
1402
|
-
|
|
1872
|
+
const next = this.fork();
|
|
1873
|
+
next._options.hint = hint;
|
|
1874
|
+
return next;
|
|
1403
1875
|
}
|
|
1404
1876
|
/** Set the driver cursor batch size. */
|
|
1405
1877
|
batchSize(n) {
|
|
1406
|
-
|
|
1407
|
-
|
|
1878
|
+
const next = this.fork();
|
|
1879
|
+
next._options.batchSize = n;
|
|
1880
|
+
return next;
|
|
1408
1881
|
}
|
|
1409
1882
|
/** Run the query inside a transaction session. */
|
|
1410
1883
|
session(session) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1884
|
+
const next = this.fork();
|
|
1885
|
+
next._options.session = session;
|
|
1886
|
+
return next;
|
|
1413
1887
|
}
|
|
1414
1888
|
/** Cap server-side execution time. */
|
|
1415
1889
|
maxTimeMS(ms) {
|
|
1416
|
-
|
|
1417
|
-
|
|
1890
|
+
const next = this.fork();
|
|
1891
|
+
next._options.maxTimeMS = ms;
|
|
1892
|
+
return next;
|
|
1418
1893
|
}
|
|
1419
1894
|
/** Bypass the read cache for this query when `false`. */
|
|
1420
1895
|
cache(on) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1896
|
+
const next = this.fork();
|
|
1897
|
+
next._options.cache = on;
|
|
1898
|
+
return next;
|
|
1423
1899
|
}
|
|
1424
1900
|
/** Override in-flight dedup for this query when set. */
|
|
1425
1901
|
dedupe(on) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1902
|
+
const next = this.fork();
|
|
1903
|
+
next._options.dedupe = on;
|
|
1904
|
+
return next;
|
|
1428
1905
|
}
|
|
1429
1906
|
/** Compile the builder into driver options (does not execute). */
|
|
1430
1907
|
compile() {
|
|
@@ -1466,7 +1943,7 @@ var QueryBuilder = class {
|
|
|
1466
1943
|
options
|
|
1467
1944
|
);
|
|
1468
1945
|
}
|
|
1469
|
-
/** Streaming cursor (raw driver cursor; no retry
|
|
1946
|
+
/** Streaming cursor (raw driver cursor; no retry/cache/drift — caller-owned). */
|
|
1470
1947
|
cursor() {
|
|
1471
1948
|
return this.ctx.collection.find(
|
|
1472
1949
|
this._filter,
|
|
@@ -1482,9 +1959,21 @@ var QueryBuilder = class {
|
|
|
1482
1959
|
this.countOptions()
|
|
1483
1960
|
);
|
|
1484
1961
|
}
|
|
1485
|
-
/** True when at least one document matches. */
|
|
1962
|
+
/** True when at least one document matches (`_id`-only projection — no doc transfer). */
|
|
1486
1963
|
async exists() {
|
|
1487
|
-
|
|
1964
|
+
const options = this.compile();
|
|
1965
|
+
if (options.limit === void 0) options.limit = 1;
|
|
1966
|
+
options.projection = { _id: 1 };
|
|
1967
|
+
const doc = await this.ctx.run(
|
|
1968
|
+
"mongo.exists",
|
|
1969
|
+
this._filter,
|
|
1970
|
+
(opts) => this.ctx.collection.findOne(
|
|
1971
|
+
this._filter,
|
|
1972
|
+
opts
|
|
1973
|
+
),
|
|
1974
|
+
options
|
|
1975
|
+
);
|
|
1976
|
+
return doc !== null;
|
|
1488
1977
|
}
|
|
1489
1978
|
};
|
|
1490
1979
|
|
|
@@ -1515,8 +2004,9 @@ var createRepository = (manager, collection, options = {}) => {
|
|
|
1515
2004
|
findOne: (filter, o) => manager.getOne(collection, filter, o),
|
|
1516
2005
|
findOneOrFail: (filter, o) => manager.getOneOrFail(collection, filter, o),
|
|
1517
2006
|
findMany: (filter, o) => manager.findMany(collection, filter, {
|
|
1518
|
-
|
|
1519
|
-
...options.limit !== void 0 ? { limit: options.limit } : {}
|
|
2007
|
+
// Repository `limit` is a DEFAULT — a per-call option must win.
|
|
2008
|
+
...options.limit !== void 0 ? { limit: options.limit } : {},
|
|
2009
|
+
...o ?? {}
|
|
1520
2010
|
}),
|
|
1521
2011
|
findActive: (filter, o) => manager.findActive(collection, filter, o),
|
|
1522
2012
|
count: (filter, o) => manager.countDocuments(collection, filter, o),
|
|
@@ -1544,9 +2034,25 @@ var createRepository = (manager, collection, options = {}) => {
|
|
|
1544
2034
|
|
|
1545
2035
|
// src/schema/collections.ts
|
|
1546
2036
|
var resolveItemName = (item) => "schema" in item ? item.schema.name : item.name;
|
|
2037
|
+
var assertTimestampsDeclared = (name, schema, timestamps) => {
|
|
2038
|
+
const ts = typeof timestamps === "boolean" ? {} : timestamps;
|
|
2039
|
+
const createdAt = ts.createdAt ?? "createdAt";
|
|
2040
|
+
const updatedAt = ts.updatedAt ?? "updatedAt";
|
|
2041
|
+
const properties = schema.properties ?? {};
|
|
2042
|
+
for (const field of [createdAt, updatedAt]) {
|
|
2043
|
+
if (!(field in properties)) {
|
|
2044
|
+
throw new Error(
|
|
2045
|
+
`defineCollections: "${name}" enables timestamps but its schema does not declare the field "${field}". Add \`${field}: s.date().optional()\` to the schema (or adjust \`timestamps\`) \u2014 otherwise strict validation rejects every stamped write.`
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
1547
2050
|
var resolveItemValue = (item) => {
|
|
1548
2051
|
if ("schema" in item) {
|
|
1549
2052
|
const def = item;
|
|
2053
|
+
if (def.timestamps !== void 0) {
|
|
2054
|
+
assertTimestampsDeclared(def.name, def.schema, def.timestamps);
|
|
2055
|
+
}
|
|
1550
2056
|
return {
|
|
1551
2057
|
schema: def.schema,
|
|
1552
2058
|
...def.indexes ? { indexes: def.indexes } : {},
|
|
@@ -1558,6 +2064,9 @@ var resolveItemValue = (item) => {
|
|
|
1558
2064
|
};
|
|
1559
2065
|
var defineCollection = (name, schema, extras = {}) => {
|
|
1560
2066
|
const namedSchema = { ...schema, name };
|
|
2067
|
+
if (extras.timestamps !== void 0) {
|
|
2068
|
+
assertTimestampsDeclared(name, schema, extras.timestamps);
|
|
2069
|
+
}
|
|
1561
2070
|
return {
|
|
1562
2071
|
name,
|
|
1563
2072
|
schema: namedSchema,
|
|
@@ -1587,6 +2096,11 @@ var addNumericBounds = (out, field) => {
|
|
|
1587
2096
|
if (field.exclusiveMaximum) out.exclusiveMaximum = true;
|
|
1588
2097
|
return out;
|
|
1589
2098
|
};
|
|
2099
|
+
var NUMERIC_WIRE_UNION = {
|
|
2100
|
+
double: ["double", "int", "long"],
|
|
2101
|
+
long: ["long", "int", "double"]
|
|
2102
|
+
};
|
|
2103
|
+
var INTEGER_WIRE_UNION = ["int", "long", "double"];
|
|
1590
2104
|
var convertField = (field) => {
|
|
1591
2105
|
if (field.kind === "raw") {
|
|
1592
2106
|
const out2 = { ...field.fragment };
|
|
@@ -1603,13 +2117,15 @@ var convertField = (field) => {
|
|
|
1603
2117
|
if (field.pattern) out.pattern = field.pattern;
|
|
1604
2118
|
return out;
|
|
1605
2119
|
case "number":
|
|
1606
|
-
out.bsonType = field.integer ?
|
|
2120
|
+
out.bsonType = field.integer ? [...INTEGER_WIRE_UNION] : "number";
|
|
2121
|
+
if (field.integer && field.multipleOf === void 0) out.multipleOf = 1;
|
|
1607
2122
|
return addNumericBounds(out, field);
|
|
1608
2123
|
case "double":
|
|
1609
|
-
out.bsonType =
|
|
2124
|
+
out.bsonType = [...NUMERIC_WIRE_UNION.double];
|
|
1610
2125
|
return addNumericBounds(out, field);
|
|
1611
2126
|
case "long":
|
|
1612
|
-
out.bsonType =
|
|
2127
|
+
out.bsonType = [...NUMERIC_WIRE_UNION.long];
|
|
2128
|
+
if (field.multipleOf === void 0) out.multipleOf = 1;
|
|
1613
2129
|
return addNumericBounds(out, field);
|
|
1614
2130
|
case "decimal":
|
|
1615
2131
|
out.bsonType = "decimal";
|
|
@@ -1625,6 +2141,7 @@ var convertField = (field) => {
|
|
|
1625
2141
|
return out;
|
|
1626
2142
|
case "geoPoint":
|
|
1627
2143
|
return {
|
|
2144
|
+
...field.description ? { description: field.description } : {},
|
|
1628
2145
|
bsonType: "object",
|
|
1629
2146
|
required: ["type", "coordinates"],
|
|
1630
2147
|
properties: {
|
|
@@ -1650,7 +2167,10 @@ var convertField = (field) => {
|
|
|
1650
2167
|
else if (typeof v === "string") types.add("string");
|
|
1651
2168
|
else if (typeof v === "number") {
|
|
1652
2169
|
const allInt = field.values.every((x) => typeof x !== "number" || Number.isInteger(x));
|
|
1653
|
-
|
|
2170
|
+
const fitsInt32 = allInt && field.values.every(
|
|
2171
|
+
(x) => typeof x !== "number" || x >= -2147483648 && x <= 2147483647
|
|
2172
|
+
);
|
|
2173
|
+
types.add(allInt && fitsInt32 ? "int" : "number");
|
|
1654
2174
|
break;
|
|
1655
2175
|
}
|
|
1656
2176
|
}
|
|
@@ -1804,6 +2324,10 @@ var numericValue = (value) => {
|
|
|
1804
2324
|
const n = Number(value.toString());
|
|
1805
2325
|
return Number.isNaN(n) ? void 0 : n;
|
|
1806
2326
|
}
|
|
2327
|
+
if (value instanceof Long) {
|
|
2328
|
+
const n = value.toNumber();
|
|
2329
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
2330
|
+
}
|
|
1807
2331
|
if (typeof value === "number" && !Number.isNaN(value)) return value;
|
|
1808
2332
|
return void 0;
|
|
1809
2333
|
};
|
|
@@ -1861,7 +2385,7 @@ var bsonTypeMatches = (bsonType, value) => {
|
|
|
1861
2385
|
case "double":
|
|
1862
2386
|
return typeof value === "number" && !Number.isNaN(value);
|
|
1863
2387
|
case "long":
|
|
1864
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
2388
|
+
return typeof value === "number" && Number.isInteger(value) || value instanceof Long;
|
|
1865
2389
|
case "decimal":
|
|
1866
2390
|
return value instanceof Decimal128;
|
|
1867
2391
|
default:
|
|
@@ -1883,6 +2407,19 @@ var checkReservedValue = (key, value, path, issues) => {
|
|
|
1883
2407
|
};
|
|
1884
2408
|
|
|
1885
2409
|
// src/schema/validate-doc/field-validators.ts
|
|
2410
|
+
var patternCache = /* @__PURE__ */ new WeakMap();
|
|
2411
|
+
var compiledPattern = (field) => {
|
|
2412
|
+
if (!field.pattern) return void 0;
|
|
2413
|
+
const cached = patternCache.get(field);
|
|
2414
|
+
if (cached) return cached;
|
|
2415
|
+
try {
|
|
2416
|
+
const re = new RegExp(field.pattern);
|
|
2417
|
+
patternCache.set(field, re);
|
|
2418
|
+
return re;
|
|
2419
|
+
} catch {
|
|
2420
|
+
return void 0;
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
1886
2423
|
var stringValidator = (_deps, field, value, path, issues) => {
|
|
1887
2424
|
const f = field;
|
|
1888
2425
|
if (typeof value !== "string") {
|
|
@@ -1907,19 +2444,15 @@ var stringValidator = (_deps, field, value, path, issues) => {
|
|
|
1907
2444
|
`Length ${value.length} > ${f.maxLength}`
|
|
1908
2445
|
);
|
|
1909
2446
|
}
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
);
|
|
1920
|
-
}
|
|
1921
|
-
} catch {
|
|
1922
|
-
}
|
|
2447
|
+
const pattern = compiledPattern(f);
|
|
2448
|
+
if (pattern && !pattern.test(value)) {
|
|
2449
|
+
push(
|
|
2450
|
+
issues,
|
|
2451
|
+
path,
|
|
2452
|
+
"constraint",
|
|
2453
|
+
`string(pattern ${f.pattern})`,
|
|
2454
|
+
`"${value}" does not match pattern`
|
|
2455
|
+
);
|
|
1923
2456
|
}
|
|
1924
2457
|
};
|
|
1925
2458
|
var numberValidator = (_deps, field, value, path, issues) => {
|
|
@@ -1936,7 +2469,7 @@ var numberValidator = (_deps, field, value, path, issues) => {
|
|
|
1936
2469
|
};
|
|
1937
2470
|
var floatingValidator = (_deps, field, value, path, issues) => {
|
|
1938
2471
|
const f = field;
|
|
1939
|
-
const ok = f.kind === "decimal" ? value instanceof Decimal128 : f.kind === "long" ? typeof value === "number" && Number.isInteger(value) : typeof value === "number" && !Number.isNaN(value);
|
|
2472
|
+
const ok = f.kind === "decimal" ? value instanceof Decimal128 : f.kind === "long" ? typeof value === "number" && Number.isInteger(value) || value instanceof Long : typeof value === "number" && !Number.isNaN(value);
|
|
1940
2473
|
if (!ok) {
|
|
1941
2474
|
push(issues, path, "type", f.kind, `Expected ${f.kind}, got ${describeValue(value)}`);
|
|
1942
2475
|
return;
|
|
@@ -2093,7 +2626,7 @@ function validateObjectProps(field, value, path, issues, topLevel) {
|
|
|
2093
2626
|
validateValue(prop, value[key], propPath(key), issues);
|
|
2094
2627
|
}
|
|
2095
2628
|
if (field.additionalProperties !== true) {
|
|
2096
|
-
const allowedReserved = topLevel ?
|
|
2629
|
+
const allowedReserved = topLevel ? ALLOWED_RESERVED_TOP : NO_RESERVED;
|
|
2097
2630
|
for (const key of Object.keys(value)) {
|
|
2098
2631
|
if (key in field.properties) continue;
|
|
2099
2632
|
if (allowedReserved.has(key)) continue;
|
|
@@ -2140,6 +2673,11 @@ function validateObjectProps(field, value, path, issues, topLevel) {
|
|
|
2140
2673
|
}
|
|
2141
2674
|
}
|
|
2142
2675
|
var deps = { validateValue, validateObjectProps };
|
|
2676
|
+
var ALLOWED_RESERVED_TOP = /* @__PURE__ */ new Set([
|
|
2677
|
+
"_id",
|
|
2678
|
+
...Object.keys(ORM_RESERVED_FIELDS)
|
|
2679
|
+
]);
|
|
2680
|
+
var NO_RESERVED = /* @__PURE__ */ new Set();
|
|
2143
2681
|
var validateDoc = (schema, doc) => {
|
|
2144
2682
|
const issues = [];
|
|
2145
2683
|
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
@@ -2166,15 +2704,23 @@ var createResolveCollectionName = (config) => {
|
|
|
2166
2704
|
// src/service/connection-uri.ts
|
|
2167
2705
|
var normalizeMongoUrl = (rawUrl) => {
|
|
2168
2706
|
try {
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
const
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2707
|
+
const qIndex = rawUrl.indexOf("?");
|
|
2708
|
+
const base = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
|
|
2709
|
+
const query = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
|
|
2710
|
+
const params = new URLSearchParams(query);
|
|
2711
|
+
if (!params.has("retryWrites")) params.set("retryWrites", "false");
|
|
2712
|
+
const hasWriteConcern = params.has("w") || params.has("wtimeoutMS") || params.has("writeConcern");
|
|
2713
|
+
if (!hasWriteConcern) params.set("w", "1");
|
|
2714
|
+
const schemeSep = base.indexOf("://");
|
|
2715
|
+
const authority = schemeSep === -1 ? base : base.slice(schemeSep + 3).split("/")[0] ?? "";
|
|
2716
|
+
const hostPart = authority.includes("@") ? authority.split("@").pop() ?? "" : authority;
|
|
2717
|
+
const hostCount = hostPart === "" ? 0 : hostPart.split(",").length;
|
|
2718
|
+
const isSrv = base.startsWith("mongodb+srv://");
|
|
2719
|
+
if (!isSrv && hostCount === 1 && !params.has("replicaSet") && !params.has("directConnection")) {
|
|
2720
|
+
params.set("directConnection", "true");
|
|
2176
2721
|
}
|
|
2177
|
-
|
|
2722
|
+
const queryString = params.toString();
|
|
2723
|
+
return queryString ? `${base}?${queryString}` : base;
|
|
2178
2724
|
} catch {
|
|
2179
2725
|
return rawUrl;
|
|
2180
2726
|
}
|
|
@@ -2210,7 +2756,12 @@ var CacheInvalidator = class {
|
|
|
2210
2756
|
const key = `${ref.db.databaseName}${SEP2}${ref.collection}`;
|
|
2211
2757
|
if (seen.has(key) || this.streams.has(key)) continue;
|
|
2212
2758
|
seen.add(key);
|
|
2213
|
-
void this.watchLoop(key, ref)
|
|
2759
|
+
void this.watchLoop(key, ref).catch((err) => {
|
|
2760
|
+
this.logger.error?.(
|
|
2761
|
+
{ error: err instanceof Error ? err.message : String(err), collection: ref.collection },
|
|
2762
|
+
"cacheWatch: watcher crashed"
|
|
2763
|
+
);
|
|
2764
|
+
});
|
|
2214
2765
|
}
|
|
2215
2766
|
}
|
|
2216
2767
|
/** Close every watcher. Safe to call repeatedly, and before any `start`. */
|
|
@@ -2230,11 +2781,17 @@ var CacheInvalidator = class {
|
|
|
2230
2781
|
async watchLoop(key, ref) {
|
|
2231
2782
|
let backoff = 0;
|
|
2232
2783
|
while (!this.stopped) {
|
|
2784
|
+
let connectedAt = Date.now();
|
|
2233
2785
|
try {
|
|
2234
2786
|
const stream = ref.db.collection(ref.collection).watch([]);
|
|
2235
|
-
stream.on("error", () => {
|
|
2787
|
+
stream.on("error", (err) => {
|
|
2788
|
+
this.logger.debug?.(
|
|
2789
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
2790
|
+
"cacheWatch: stream error event"
|
|
2791
|
+
);
|
|
2236
2792
|
});
|
|
2237
2793
|
this.streams.set(key, stream);
|
|
2794
|
+
connectedAt = Date.now();
|
|
2238
2795
|
if (backoff > 0) {
|
|
2239
2796
|
this.cache.invalidateByCollection(
|
|
2240
2797
|
cacheCollectionKey(ref.db.databaseName, ref.collection)
|
|
@@ -2247,6 +2804,7 @@ var CacheInvalidator = class {
|
|
|
2247
2804
|
);
|
|
2248
2805
|
}
|
|
2249
2806
|
} catch (err) {
|
|
2807
|
+
if (Date.now() - connectedAt > 6e4) backoff = 0;
|
|
2250
2808
|
const message = err instanceof Error ? err.message : String(err);
|
|
2251
2809
|
const abandoned = this.streams.get(key);
|
|
2252
2810
|
this.streams.delete(key);
|
|
@@ -2256,13 +2814,13 @@ var CacheInvalidator = class {
|
|
|
2256
2814
|
} catch {
|
|
2257
2815
|
}
|
|
2258
2816
|
}
|
|
2259
|
-
if (
|
|
2817
|
+
if (isPermanentWatchError(message)) {
|
|
2260
2818
|
this.disabled = true;
|
|
2261
2819
|
if (!this.warnedUnsupported) {
|
|
2262
2820
|
this.warnedUnsupported = true;
|
|
2263
2821
|
this.logger.warn?.(
|
|
2264
2822
|
{ collection: ref.collection, error: message },
|
|
2265
|
-
"cacheWatch: change streams
|
|
2823
|
+
"cacheWatch: change streams unavailable permanently \u2014 the query cache stays write-through only; set cache: { ttlMs } or disable cacheWatch"
|
|
2266
2824
|
);
|
|
2267
2825
|
}
|
|
2268
2826
|
return;
|
|
@@ -2273,7 +2831,7 @@ var CacheInvalidator = class {
|
|
|
2273
2831
|
"cacheWatch: change stream error, retrying with backoff"
|
|
2274
2832
|
);
|
|
2275
2833
|
backoff = Math.min(backoff + 1e3, 5e3);
|
|
2276
|
-
await sleepJittered(backoff);
|
|
2834
|
+
await sleepJittered(backoff, 1e3, true);
|
|
2277
2835
|
}
|
|
2278
2836
|
}
|
|
2279
2837
|
}
|
|
@@ -2352,6 +2910,7 @@ var makeGetDbUrl = (dbClients, config) => (name) => {
|
|
|
2352
2910
|
return normalizeMongoUrl(config.defaultDb);
|
|
2353
2911
|
};
|
|
2354
2912
|
var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClients = /* @__PURE__ */ new Map()) => {
|
|
2913
|
+
const connecting = /* @__PURE__ */ new Map();
|
|
2355
2914
|
return async (names) => {
|
|
2356
2915
|
const keys = names && names.length > 0 ? names : Object.keys(dbClients);
|
|
2357
2916
|
const openedThisCall = [];
|
|
@@ -2362,18 +2921,32 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
|
|
|
2362
2921
|
const dbUrl = getDbUrl(key);
|
|
2363
2922
|
let client = openedClients.get(dbUrl);
|
|
2364
2923
|
if (!client) {
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2924
|
+
const inflight = connecting.get(dbUrl);
|
|
2925
|
+
if (inflight) {
|
|
2926
|
+
client = await inflight;
|
|
2927
|
+
} else {
|
|
2928
|
+
const attempt = (async () => {
|
|
2929
|
+
const c = new MongoClient(dbUrl, {
|
|
2930
|
+
connectTimeoutMS: definition.connectTimeoutMs ?? 1e4,
|
|
2931
|
+
serverSelectionTimeoutMS: definition.connectTimeoutMs ?? 1e4,
|
|
2932
|
+
...definition.readPreference && { readPreference: definition.readPreference },
|
|
2933
|
+
...definition.readConcern && { readConcern: definition.readConcern },
|
|
2934
|
+
...definition.writeConcern && { writeConcern: definition.writeConcern }
|
|
2935
|
+
});
|
|
2936
|
+
c.on("error", () => {
|
|
2937
|
+
});
|
|
2938
|
+
await c.connect();
|
|
2939
|
+
return c;
|
|
2940
|
+
})();
|
|
2941
|
+
connecting.set(dbUrl, attempt);
|
|
2942
|
+
try {
|
|
2943
|
+
client = await attempt;
|
|
2944
|
+
openedClients.set(dbUrl, client);
|
|
2945
|
+
openedThisCall.push({ url: dbUrl, client });
|
|
2946
|
+
} finally {
|
|
2947
|
+
connecting.delete(dbUrl);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2377
2950
|
}
|
|
2378
2951
|
const dbKey = `${key}Client`;
|
|
2379
2952
|
db[dbKey] = buildManager2(key, definition.name, client);
|
|
@@ -2401,36 +2974,45 @@ var closeConnections = async (openedClients) => {
|
|
|
2401
2974
|
};
|
|
2402
2975
|
|
|
2403
2976
|
// src/service/health.ts
|
|
2404
|
-
var createHealth = (db) => async () => {
|
|
2977
|
+
var createHealth = (db, options = {}) => async () => {
|
|
2405
2978
|
const started = performance.now();
|
|
2979
|
+
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
2406
2980
|
const dbs = await Promise.all(
|
|
2407
2981
|
Object.entries(db).map(async ([key, manager]) => {
|
|
2408
2982
|
const name = key.replace(/Client$/, "");
|
|
2409
2983
|
const handle = manager?.client;
|
|
2410
|
-
if (!handle) return { name, ok: false };
|
|
2984
|
+
if (!handle) return { name, ok: false, error: "no client handle on manager" };
|
|
2411
2985
|
try {
|
|
2412
2986
|
const t0 = performance.now();
|
|
2413
|
-
await withTimeout(handle.admin().command({ ping: 1 }),
|
|
2987
|
+
await withTimeout(handle.admin().command({ ping: 1 }), timeoutMs, `health:${name}`);
|
|
2414
2988
|
return { name, ok: true, latencyMs: performance.now() - t0 };
|
|
2415
|
-
} catch {
|
|
2416
|
-
return {
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
return {
|
|
2991
|
+
name,
|
|
2992
|
+
ok: false,
|
|
2993
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2994
|
+
};
|
|
2417
2995
|
}
|
|
2418
2996
|
})
|
|
2419
2997
|
);
|
|
2420
|
-
return {
|
|
2998
|
+
return {
|
|
2999
|
+
ok: dbs.length > 0 && dbs.every((r) => r.ok),
|
|
3000
|
+
latencyMs: performance.now() - started,
|
|
3001
|
+
dbs
|
|
3002
|
+
};
|
|
2421
3003
|
};
|
|
2422
3004
|
|
|
2423
3005
|
// src/relation/populate.ts
|
|
2424
3006
|
var createForeignKeyLoader = (deps2, collection, foreignField, options = {}) => new DataLoader({
|
|
2425
|
-
maxBatchSize: options.maxBatchSize,
|
|
2426
|
-
cache: options.cache,
|
|
3007
|
+
...options.maxBatchSize !== void 0 ? { maxBatchSize: options.maxBatchSize } : {},
|
|
3008
|
+
...options.cache !== void 0 ? { cache: options.cache } : {},
|
|
2427
3009
|
keyOf: canonicalKey,
|
|
2428
3010
|
batch: async (keys) => {
|
|
2429
3011
|
const docs = await deps2.findMany(
|
|
2430
3012
|
collection,
|
|
2431
3013
|
{ [foreignField]: { $in: keys } },
|
|
2432
3014
|
{
|
|
2433
|
-
limit: options.maxDocs
|
|
3015
|
+
...options.maxDocs !== void 0 ? { limit: options.maxDocs } : {}
|
|
2434
3016
|
}
|
|
2435
3017
|
);
|
|
2436
3018
|
const index = /* @__PURE__ */ new Map();
|
|
@@ -2463,57 +3045,72 @@ var distinctKeys = (values) => {
|
|
|
2463
3045
|
};
|
|
2464
3046
|
var makePopulator = (deps2) => {
|
|
2465
3047
|
const populate = async (docs, relations, options = {}) => {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const
|
|
2510
|
-
|
|
3048
|
+
const out = docs.map((doc) => ({ ...doc }));
|
|
3049
|
+
if (out.length === 0 || relations.length === 0) return out;
|
|
3050
|
+
const loaderCache = /* @__PURE__ */ new Map();
|
|
3051
|
+
const loaderFor = (collection, foreignField) => {
|
|
3052
|
+
const cacheKey = `${collection}\0${foreignField}`;
|
|
3053
|
+
let loader = loaderCache.get(cacheKey);
|
|
3054
|
+
if (!loader) {
|
|
3055
|
+
loader = createForeignKeyLoader(deps2, collection, foreignField, options);
|
|
3056
|
+
loaderCache.set(cacheKey, loader);
|
|
3057
|
+
}
|
|
3058
|
+
return loader;
|
|
3059
|
+
};
|
|
3060
|
+
const loadIndex = async (collection, foreignField, keys) => {
|
|
3061
|
+
const values = await loaderFor(collection, foreignField).loadMany(keys);
|
|
3062
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3063
|
+
keys.forEach((key, i) => {
|
|
3064
|
+
byKey.set(canonicalKey(key), values[i] ?? []);
|
|
3065
|
+
});
|
|
3066
|
+
return byKey;
|
|
3067
|
+
};
|
|
3068
|
+
await Promise.all(
|
|
3069
|
+
relations.map(async (relation) => {
|
|
3070
|
+
const target = relation.foreignField ?? "_id";
|
|
3071
|
+
const sourceKeys = distinctKeys(out.map((doc) => doc[relation.localField]));
|
|
3072
|
+
if (relation.type === "belongsTo") {
|
|
3073
|
+
const byKey = await loadIndex(relation.collection, target, sourceKeys);
|
|
3074
|
+
for (const doc of out) {
|
|
3075
|
+
const key = doc[relation.localField];
|
|
3076
|
+
const matches = key === void 0 || key === null ? void 0 : byKey.get(canonicalKey(key));
|
|
3077
|
+
doc[relation.as] = matches?.[0] ?? (relation.optional ? void 0 : null);
|
|
3078
|
+
}
|
|
3079
|
+
} else if (relation.type === "hasMany") {
|
|
3080
|
+
const byKey = await loadIndex(relation.collection, target, sourceKeys);
|
|
3081
|
+
for (const doc of out) {
|
|
3082
|
+
const key = doc[relation.localField];
|
|
3083
|
+
doc[relation.as] = key === void 0 || key === null ? [] : byKey.get(canonicalKey(key)) ?? [];
|
|
3084
|
+
}
|
|
3085
|
+
} else {
|
|
3086
|
+
const throughByKey = await loadIndex(
|
|
3087
|
+
relation.through.collection,
|
|
3088
|
+
relation.through.localField,
|
|
3089
|
+
sourceKeys
|
|
3090
|
+
);
|
|
3091
|
+
const joinDocs = [...throughByKey.values()].flat();
|
|
3092
|
+
const targetKeys = distinctKeys(
|
|
3093
|
+
joinDocs.map((join3) => join3[relation.through.foreignField])
|
|
2511
3094
|
);
|
|
2512
|
-
|
|
3095
|
+
const targetByKey = await loadIndex(relation.collection, target, targetKeys);
|
|
3096
|
+
for (const doc of out) {
|
|
3097
|
+
const key = doc[relation.localField];
|
|
3098
|
+
const joins = key === void 0 || key === null ? [] : throughByKey.get(canonicalKey(key)) ?? [];
|
|
3099
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
3100
|
+
const targets = [];
|
|
3101
|
+
for (const join3 of joins) {
|
|
3102
|
+
const tk = canonicalKey(join3[relation.through.foreignField]);
|
|
3103
|
+
if (seenTargets.has(tk)) continue;
|
|
3104
|
+
seenTargets.add(tk);
|
|
3105
|
+
const bucket = targetByKey.get(tk);
|
|
3106
|
+
if (bucket) targets.push(...bucket);
|
|
3107
|
+
}
|
|
3108
|
+
doc[relation.as] = targets;
|
|
3109
|
+
}
|
|
2513
3110
|
}
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
return
|
|
3111
|
+
})
|
|
3112
|
+
);
|
|
3113
|
+
return out;
|
|
2517
3114
|
};
|
|
2518
3115
|
return { populate };
|
|
2519
3116
|
};
|
|
@@ -2535,11 +3132,23 @@ var asCollectionDefinition = (value) => {
|
|
|
2535
3132
|
};
|
|
2536
3133
|
|
|
2537
3134
|
// src/service/trace-db-op.ts
|
|
3135
|
+
var debugQueryCache;
|
|
3136
|
+
var probeDebugQuery = async () => {
|
|
3137
|
+
if (debugQueryCache !== void 0) return;
|
|
3138
|
+
try {
|
|
3139
|
+
const mod = await import('@ignex/core/debug');
|
|
3140
|
+
debugQueryCache = typeof mod.debugQuery === "function" ? mod.debugQuery : null;
|
|
3141
|
+
} catch {
|
|
3142
|
+
debugQueryCache = null;
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
2538
3145
|
var traceDbOp = async (logger, meta, fn, options = {}) => {
|
|
2539
3146
|
const start = performance.now();
|
|
2540
3147
|
logger.debug({ ...meta, operation: "start" }, "start");
|
|
2541
3148
|
try {
|
|
2542
|
-
|
|
3149
|
+
if (debugQueryCache === void 0) await probeDebugQuery();
|
|
3150
|
+
const debugQuery = debugQueryCache;
|
|
3151
|
+
const result = debugQuery ? await debugQuery(`${meta.collection}.${meta.op}`, void 0, () => fn()) : await fn();
|
|
2543
3152
|
const durationMs = performance.now() - start;
|
|
2544
3153
|
logger.info({ ...meta, durationMs }, "ok");
|
|
2545
3154
|
return result;
|
|
@@ -2584,10 +3193,16 @@ var resolveQueryOptions = (options) => {
|
|
|
2584
3193
|
return {
|
|
2585
3194
|
maxAttempts,
|
|
2586
3195
|
retryDelayMs,
|
|
2587
|
-
dedupe,
|
|
2588
|
-
cache,
|
|
2589
|
-
retryWrites,
|
|
2590
|
-
sdk: {
|
|
3196
|
+
...dedupe !== void 0 ? { dedupe } : {},
|
|
3197
|
+
...cache !== void 0 ? { cache } : {},
|
|
3198
|
+
...retryWrites !== void 0 ? { retryWrites } : {},
|
|
3199
|
+
sdk: {
|
|
3200
|
+
...session !== void 0 ? { session } : {},
|
|
3201
|
+
...maxTimeMS !== void 0 ? { maxTimeMS } : {},
|
|
3202
|
+
...hint !== void 0 ? { hint } : {},
|
|
3203
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
3204
|
+
...drift !== void 0 ? { drift } : {}
|
|
3205
|
+
},
|
|
2591
3206
|
driverOpts: rest
|
|
2592
3207
|
};
|
|
2593
3208
|
};
|
|
@@ -2699,7 +3314,7 @@ var PipelineBuilder = class _PipelineBuilder {
|
|
|
2699
3314
|
collection: this.deps.logical,
|
|
2700
3315
|
opName: "mongo.pipeline",
|
|
2701
3316
|
pipeline: this.stages,
|
|
2702
|
-
options: this.deps.options,
|
|
3317
|
+
...this.deps.options !== void 0 ? { options: this.deps.options } : {},
|
|
2703
3318
|
execute: run
|
|
2704
3319
|
});
|
|
2705
3320
|
}
|
|
@@ -2726,7 +3341,7 @@ var PipelineBuilder = class _PipelineBuilder {
|
|
|
2726
3341
|
collection: this.deps.logical,
|
|
2727
3342
|
opName: "mongo.pipeline.first",
|
|
2728
3343
|
pipeline: this.stages,
|
|
2729
|
-
options: this.deps.options,
|
|
3344
|
+
...this.deps.options !== void 0 ? { options: this.deps.options } : {},
|
|
2730
3345
|
execute: run
|
|
2731
3346
|
});
|
|
2732
3347
|
}
|
|
@@ -2863,7 +3478,7 @@ var makeAggregateOps = (ctx) => {
|
|
|
2863
3478
|
coll: () => coll(collection),
|
|
2864
3479
|
crudDeps: deps2,
|
|
2865
3480
|
mergeDriver: mergeAggOptions2,
|
|
2866
|
-
options,
|
|
3481
|
+
...options !== void 0 ? { options } : {},
|
|
2867
3482
|
cachedAggregate
|
|
2868
3483
|
});
|
|
2869
3484
|
return { aggregate, pipeline };
|
|
@@ -2872,10 +3487,10 @@ var makeAggregateOps = (ctx) => {
|
|
|
2872
3487
|
// src/service/aggregation/helpers.ts
|
|
2873
3488
|
var mergeAggOptions = (driverOpts, sdk) => ({
|
|
2874
3489
|
...driverOpts,
|
|
2875
|
-
session: sdk.session,
|
|
2876
|
-
maxTimeMS: sdk.maxTimeMS,
|
|
2877
|
-
hint: sdk.hint,
|
|
2878
|
-
batchSize: sdk.batchSize
|
|
3490
|
+
...sdk.session !== void 0 ? { session: sdk.session } : {},
|
|
3491
|
+
...sdk.maxTimeMS !== void 0 ? { maxTimeMS: sdk.maxTimeMS } : {},
|
|
3492
|
+
...sdk.hint !== void 0 ? { hint: sdk.hint } : {},
|
|
3493
|
+
...sdk.batchSize !== void 0 ? { batchSize: sdk.batchSize } : {}
|
|
2879
3494
|
});
|
|
2880
3495
|
var DATE_PART_FORMATS = {
|
|
2881
3496
|
hour: "%Y-%m-%dT%H",
|
|
@@ -2971,10 +3586,12 @@ var createCachedAggregate = (ctx) => {
|
|
|
2971
3586
|
return runDriver();
|
|
2972
3587
|
};
|
|
2973
3588
|
if (shouldDedupe) {
|
|
2974
|
-
const
|
|
2975
|
-
|
|
3589
|
+
const primaryKey = cacheCollectionKey(client.databaseName, physical);
|
|
3590
|
+
const gen = cache !== void 0 ? cache.versionOf(primaryKey) : "";
|
|
3591
|
+
const dedupeKey = `${primaryKey}|${gen}|${opName}|${stableHash([pipeline, options])}`;
|
|
3592
|
+
return await opts.inFlight.run(dedupeKey, runOnce);
|
|
2976
3593
|
}
|
|
2977
|
-
return runOnce();
|
|
3594
|
+
return await runOnce();
|
|
2978
3595
|
};
|
|
2979
3596
|
};
|
|
2980
3597
|
|
|
@@ -2997,7 +3614,7 @@ var makeGroupOps = (ctx) => {
|
|
|
2997
3614
|
collection,
|
|
2998
3615
|
opName: "mongo.groupBy",
|
|
2999
3616
|
pipeline,
|
|
3000
|
-
options,
|
|
3617
|
+
...options !== void 0 ? { options } : {},
|
|
3001
3618
|
execute: (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray()
|
|
3002
3619
|
});
|
|
3003
3620
|
};
|
|
@@ -3027,7 +3644,7 @@ var makeGroupOps = (ctx) => {
|
|
|
3027
3644
|
collection,
|
|
3028
3645
|
opName: "mongo.dateRangeAnalysis",
|
|
3029
3646
|
pipeline,
|
|
3030
|
-
options,
|
|
3647
|
+
...options !== void 0 ? { options } : {},
|
|
3031
3648
|
execute: (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray()
|
|
3032
3649
|
});
|
|
3033
3650
|
};
|
|
@@ -3059,7 +3676,7 @@ var makeLookupJoinOp = (ctx) => {
|
|
|
3059
3676
|
collection,
|
|
3060
3677
|
opName: "mongo.lookupJoin",
|
|
3061
3678
|
pipeline,
|
|
3062
|
-
options,
|
|
3679
|
+
...options !== void 0 ? { options } : {},
|
|
3063
3680
|
// Writes to any joined collection must invalidate the cached join.
|
|
3064
3681
|
sources: lookups.map((l) => l.fromCollection),
|
|
3065
3682
|
execute: (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray()
|
|
@@ -3133,7 +3750,10 @@ var makeTextSearchOp = (ctx) => {
|
|
|
3133
3750
|
const { coll, mergeAggOptions: mergeAggOptions2, cachedAggregate } = ctx;
|
|
3134
3751
|
const textSearch = async (collection, baseFilter, searchConfig, paginationConfig = {}, customization = {}, options) => {
|
|
3135
3752
|
const normalized = normalizePageLimit(
|
|
3136
|
-
{
|
|
3753
|
+
{
|
|
3754
|
+
...paginationConfig.page !== void 0 ? { page: paginationConfig.page } : {},
|
|
3755
|
+
...paginationConfig.limit !== void 0 ? { limit: paginationConfig.limit } : {}
|
|
3756
|
+
},
|
|
3137
3757
|
paginationConfig.maxLimit ?? DEFAULT_MAX_LIMIT
|
|
3138
3758
|
);
|
|
3139
3759
|
const searchStages = buildSearchStages(
|
|
@@ -3160,7 +3780,7 @@ var makeTextSearchOp = (ctx) => {
|
|
|
3160
3780
|
collection,
|
|
3161
3781
|
opName: "mongo.textSearch",
|
|
3162
3782
|
pipeline,
|
|
3163
|
-
options,
|
|
3783
|
+
...options !== void 0 ? { options } : {},
|
|
3164
3784
|
execute: async (r) => {
|
|
3165
3785
|
const rows = await coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray();
|
|
3166
3786
|
const row = rows[0];
|
|
@@ -3193,6 +3813,54 @@ var makeAggregationOps = (client, dbLabel, logger, opts) => {
|
|
|
3193
3813
|
};
|
|
3194
3814
|
};
|
|
3195
3815
|
|
|
3816
|
+
// src/schema/apply-defaults.ts
|
|
3817
|
+
var cloneDefault = (value) => {
|
|
3818
|
+
if (value === null || typeof value !== "object") return value;
|
|
3819
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
3820
|
+
return Array.isArray(value) ? [...value] : { ...value };
|
|
3821
|
+
};
|
|
3822
|
+
var applySchemaDefaults = (schema, doc) => {
|
|
3823
|
+
if (!schema || doc === null || typeof doc !== "object" || Array.isArray(doc)) return;
|
|
3824
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3825
|
+
if (key in doc && doc[key] !== void 0) {
|
|
3826
|
+
if (prop.kind === "object" && doc[key] !== null && typeof doc[key] === "object" && !Array.isArray(doc[key])) {
|
|
3827
|
+
applySchemaDefaults(prop, doc[key]);
|
|
3828
|
+
}
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
if (prop.flags.hasDefault) {
|
|
3832
|
+
doc[key] = cloneDefault(prop.flags.defaultValue);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
|
|
3837
|
+
// src/utils/omit-undefined.ts
|
|
3838
|
+
var stripUndefinedKeys = (doc) => {
|
|
3839
|
+
for (const key of Object.keys(doc)) {
|
|
3840
|
+
if (doc[key] === void 0) {
|
|
3841
|
+
delete doc[key];
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
return doc;
|
|
3845
|
+
};
|
|
3846
|
+
var stripUndefinedFromUpdate = (update) => {
|
|
3847
|
+
if (update === null || typeof update !== "object") return update;
|
|
3848
|
+
const doc = update;
|
|
3849
|
+
for (const operator of ["$set", "$setOnInsert"]) {
|
|
3850
|
+
const op = doc[operator];
|
|
3851
|
+
if (op !== null && typeof op === "object" && !Array.isArray(op)) {
|
|
3852
|
+
const opDoc = op;
|
|
3853
|
+
for (const key of Object.keys(opDoc)) {
|
|
3854
|
+
if (opDoc[key] === void 0) delete opDoc[key];
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
for (const key of Object.keys(doc)) {
|
|
3859
|
+
if (doc[key] === void 0) delete doc[key];
|
|
3860
|
+
}
|
|
3861
|
+
return update;
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3196
3864
|
// src/service/drift.ts
|
|
3197
3865
|
var checkDocsDrift = (options, collection, op, docs) => {
|
|
3198
3866
|
if (options.drift === "off") return;
|
|
@@ -3246,6 +3914,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3246
3914
|
};
|
|
3247
3915
|
const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
|
|
3248
3916
|
const stampCreate = (collection, doc) => {
|
|
3917
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3918
|
+
stripUndefinedKeys(doc);
|
|
3249
3919
|
const ts = timestampsFor(collection);
|
|
3250
3920
|
if (!ts) return;
|
|
3251
3921
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3253,6 +3923,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3253
3923
|
if (ts.updatedAt && doc[ts.updatedAt] === void 0) doc[ts.updatedAt] = now;
|
|
3254
3924
|
};
|
|
3255
3925
|
const stampUpdate = (collection, update) => {
|
|
3926
|
+
stripUndefinedFromUpdate(update);
|
|
3256
3927
|
const formatted = formatUpdateFilter(update);
|
|
3257
3928
|
const ts = timestampsFor(collection);
|
|
3258
3929
|
if (ts?.updatedAt) {
|
|
@@ -3262,11 +3933,13 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3262
3933
|
return formatted;
|
|
3263
3934
|
};
|
|
3264
3935
|
const stampReplace = (collection, doc) => {
|
|
3936
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3937
|
+
stripUndefinedKeys(doc);
|
|
3265
3938
|
const ts = timestampsFor(collection);
|
|
3266
3939
|
if (!ts?.updatedAt) return doc;
|
|
3267
3940
|
return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
|
|
3268
3941
|
};
|
|
3269
|
-
const read = async (collection, opName, filter, options, execute, shape = "none") => {
|
|
3942
|
+
const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
|
|
3270
3943
|
const physical = resolve(String(collection));
|
|
3271
3944
|
const resolved = resolveQueryOptions(options);
|
|
3272
3945
|
const noSession = !resolved.sdk.session;
|
|
@@ -3278,7 +3951,12 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3278
3951
|
if (resolved.driverOpts.projection !== void 0) return;
|
|
3279
3952
|
const docs = shape === "many" ? value : shape === "one" && value !== null ? [value] : [];
|
|
3280
3953
|
checkDocsDrift(
|
|
3281
|
-
{
|
|
3954
|
+
{
|
|
3955
|
+
logger,
|
|
3956
|
+
db: dbLabel,
|
|
3957
|
+
drift: mode,
|
|
3958
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
3959
|
+
},
|
|
3282
3960
|
String(collection),
|
|
3283
3961
|
opName,
|
|
3284
3962
|
docs
|
|
@@ -3291,10 +3969,22 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3291
3969
|
delayMs: resolved.retryDelayMs
|
|
3292
3970
|
})
|
|
3293
3971
|
);
|
|
3294
|
-
const
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3972
|
+
const {
|
|
3973
|
+
maxAttempts: _ma,
|
|
3974
|
+
retryDelayMs: _rd,
|
|
3975
|
+
dedupe: _dd,
|
|
3976
|
+
cache: _ca,
|
|
3977
|
+
retryWrites: _rw,
|
|
3978
|
+
...resultOptions
|
|
3979
|
+
} = options ?? {};
|
|
3980
|
+
let payloadHash;
|
|
3981
|
+
const getPayloadHash = () => {
|
|
3982
|
+
if (payloadHash === void 0) {
|
|
3983
|
+
payloadHash = stableHash([opName, keyExtra ?? "", filter, resultOptions]);
|
|
3984
|
+
}
|
|
3985
|
+
return payloadHash;
|
|
3986
|
+
};
|
|
3987
|
+
const key = useCache ? opts.cache.key(cacheCollectionKey(client.databaseName, physical), getPayloadHash()) : void 0;
|
|
3298
3988
|
const fetchAndCheck = () => executeWrapped().then((value) => {
|
|
3299
3989
|
checkDrift(value);
|
|
3300
3990
|
return value;
|
|
@@ -3313,10 +4003,11 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3313
4003
|
return fetchAndCheck();
|
|
3314
4004
|
};
|
|
3315
4005
|
if (shouldDedupe) {
|
|
3316
|
-
const
|
|
3317
|
-
|
|
4006
|
+
const colKey = cacheCollectionKey(client.databaseName, physical);
|
|
4007
|
+
const gen = opts.cache !== void 0 ? opts.cache.versionOf(colKey) : "";
|
|
4008
|
+
return await opts.inFlight.run(`${colKey}|${gen}|${getPayloadHash()}`, runOnce);
|
|
3318
4009
|
}
|
|
3319
|
-
return runOnce();
|
|
4010
|
+
return await runOnce();
|
|
3320
4011
|
};
|
|
3321
4012
|
const normalizeFindOptions = (options) => {
|
|
3322
4013
|
if (!options) return {};
|
|
@@ -3358,7 +4049,7 @@ var mergeMongoActiveFilter = (softDelete, filter) => {
|
|
|
3358
4049
|
|
|
3359
4050
|
// src/service/crud/read-ops.ts
|
|
3360
4051
|
var makeReadOps = (ctx) => {
|
|
3361
|
-
const {
|
|
4052
|
+
const { opts } = ctx;
|
|
3362
4053
|
const { coll, toDriverFilter, read, normalizeFindOptions } = ctx;
|
|
3363
4054
|
const getOne = async (collection, filter, options) => {
|
|
3364
4055
|
const f = filter ?? {};
|
|
@@ -3371,11 +4062,14 @@ var makeReadOps = (ctx) => {
|
|
|
3371
4062
|
async (r) => {
|
|
3372
4063
|
const opts2 = {
|
|
3373
4064
|
...r.driverOpts,
|
|
3374
|
-
session: r.sdk.session,
|
|
3375
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3376
|
-
hint: r.sdk.hint
|
|
4065
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4066
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4067
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3377
4068
|
};
|
|
3378
|
-
return coll(collection).findOne(
|
|
4069
|
+
return await coll(collection).findOne(
|
|
4070
|
+
toDriverFilter(f),
|
|
4071
|
+
opts2
|
|
4072
|
+
);
|
|
3379
4073
|
},
|
|
3380
4074
|
"one"
|
|
3381
4075
|
);
|
|
@@ -3416,9 +4110,9 @@ var makeReadOps = (ctx) => {
|
|
|
3416
4110
|
async (r) => {
|
|
3417
4111
|
const opts2 = {
|
|
3418
4112
|
...r.driverOpts,
|
|
3419
|
-
session: r.sdk.session,
|
|
3420
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3421
|
-
hint: r.sdk.hint
|
|
4113
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4114
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4115
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3422
4116
|
};
|
|
3423
4117
|
return coll(collection).find(toDriverFilter(filter ?? {}), opts2).toArray();
|
|
3424
4118
|
},
|
|
@@ -3465,11 +4159,11 @@ var makeReadOps = (ctx) => {
|
|
|
3465
4159
|
async (r) => {
|
|
3466
4160
|
const opts2 = {
|
|
3467
4161
|
...r.driverOpts,
|
|
3468
|
-
session: r.sdk.session,
|
|
3469
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3470
|
-
hint: r.sdk.hint
|
|
4162
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4163
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4164
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3471
4165
|
};
|
|
3472
|
-
return coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
4166
|
+
return await coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
3473
4167
|
},
|
|
3474
4168
|
"none"
|
|
3475
4169
|
);
|
|
@@ -3484,9 +4178,9 @@ var makeReadOps = (ctx) => {
|
|
|
3484
4178
|
async (r) => {
|
|
3485
4179
|
const opts2 = {
|
|
3486
4180
|
...r.driverOpts,
|
|
3487
|
-
session: r.sdk.session,
|
|
3488
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3489
|
-
hint: r.sdk.hint
|
|
4181
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4182
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4183
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3490
4184
|
};
|
|
3491
4185
|
return coll(collection).distinct(
|
|
3492
4186
|
String(field),
|
|
@@ -3494,21 +4188,24 @@ var makeReadOps = (ctx) => {
|
|
|
3494
4188
|
opts2
|
|
3495
4189
|
);
|
|
3496
4190
|
},
|
|
3497
|
-
"none"
|
|
4191
|
+
"none",
|
|
4192
|
+
String(field)
|
|
3498
4193
|
);
|
|
3499
4194
|
};
|
|
3500
|
-
const estimatedDocumentCount = async (collection, options) =>
|
|
4195
|
+
const estimatedDocumentCount = async (collection, options) => read(
|
|
3501
4196
|
collection,
|
|
3502
4197
|
"mongo.estimatedDocumentCount",
|
|
4198
|
+
{},
|
|
4199
|
+
options,
|
|
3503
4200
|
async (r) => {
|
|
3504
4201
|
const opts2 = {
|
|
3505
4202
|
...r.driverOpts,
|
|
3506
|
-
session: r.sdk.session,
|
|
3507
|
-
maxTimeMS: r.sdk.maxTimeMS
|
|
4203
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4204
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {}
|
|
3508
4205
|
};
|
|
3509
|
-
return coll(collection).estimatedDocumentCount(opts2);
|
|
4206
|
+
return await coll(collection).estimatedDocumentCount(opts2);
|
|
3510
4207
|
},
|
|
3511
|
-
|
|
4208
|
+
"none"
|
|
3512
4209
|
);
|
|
3513
4210
|
return {
|
|
3514
4211
|
getOne,
|
|
@@ -3541,11 +4238,11 @@ var makeWatchOps = (ctx) => {
|
|
|
3541
4238
|
async (r) => {
|
|
3542
4239
|
const opts = {
|
|
3543
4240
|
...r.driverOpts,
|
|
3544
|
-
session: r.sdk.session,
|
|
3545
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3546
|
-
hint: r.sdk.hint
|
|
4241
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4242
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4243
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3547
4244
|
};
|
|
3548
|
-
return execute(opts);
|
|
4245
|
+
return await execute(opts);
|
|
3549
4246
|
},
|
|
3550
4247
|
opName === "mongo.getOne" ? "one" : opName === "mongo.findMany" ? "many" : "none"
|
|
3551
4248
|
)
|
|
@@ -3562,6 +4259,20 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3562
4259
|
const { opts } = ctx;
|
|
3563
4260
|
const { coll, toDriverFilter, writeRun, invalidate, stampCreate, stampUpdate, stampReplace } = ctx;
|
|
3564
4261
|
const { getOne } = readOps;
|
|
4262
|
+
const safeAfterHooks = async (collection, name, hookCtx) => {
|
|
4263
|
+
try {
|
|
4264
|
+
await runHooks(opts.hooks, collection, name, hookCtx);
|
|
4265
|
+
} catch (err) {
|
|
4266
|
+
ctx.logger.error?.(
|
|
4267
|
+
{
|
|
4268
|
+
collection,
|
|
4269
|
+
hook: name,
|
|
4270
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4271
|
+
},
|
|
4272
|
+
`post-commit ${name} hook failed; DB write is already committed`
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
3565
4276
|
const insertOne = async (collection, doc, options) => {
|
|
3566
4277
|
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
3567
4278
|
collection: String(collection),
|
|
@@ -3574,37 +4285,40 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3574
4285
|
async (r) => {
|
|
3575
4286
|
const insertOpts = {
|
|
3576
4287
|
...r.driverOpts,
|
|
3577
|
-
session: r.sdk.session
|
|
4288
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3578
4289
|
};
|
|
3579
|
-
return coll(collection).insertOne(
|
|
4290
|
+
return await coll(collection).insertOne(
|
|
4291
|
+
doc,
|
|
4292
|
+
insertOpts
|
|
4293
|
+
);
|
|
3580
4294
|
},
|
|
3581
4295
|
options
|
|
3582
4296
|
);
|
|
3583
4297
|
invalidate(collection);
|
|
3584
|
-
await
|
|
4298
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3585
4299
|
collection: String(collection),
|
|
3586
4300
|
doc
|
|
3587
4301
|
});
|
|
3588
4302
|
return result;
|
|
3589
4303
|
};
|
|
3590
4304
|
const insertMany = async (collection, docs, options) => {
|
|
3591
|
-
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
3592
|
-
collection: String(collection),
|
|
3593
|
-
docs
|
|
3594
|
-
});
|
|
3595
|
-
for (const d of docs) stampCreate(collection, d);
|
|
3596
4305
|
if (docs.length > MAX_BATCH_OPS) {
|
|
3597
4306
|
throw new BadRequest(
|
|
3598
4307
|
`insertMany: too many docs (${docs.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
3599
4308
|
);
|
|
3600
4309
|
}
|
|
4310
|
+
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
4311
|
+
collection: String(collection),
|
|
4312
|
+
docs
|
|
4313
|
+
});
|
|
4314
|
+
for (const d of docs) stampCreate(collection, d);
|
|
3601
4315
|
const result = await writeRun(
|
|
3602
4316
|
collection,
|
|
3603
4317
|
"mongo.insertMany",
|
|
3604
4318
|
async (r) => {
|
|
3605
4319
|
const bulkOpts = {
|
|
3606
4320
|
...r.driverOpts,
|
|
3607
|
-
session: r.sdk.session
|
|
4321
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3608
4322
|
};
|
|
3609
4323
|
return coll(collection).insertMany(
|
|
3610
4324
|
docs,
|
|
@@ -3614,7 +4328,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3614
4328
|
options
|
|
3615
4329
|
);
|
|
3616
4330
|
invalidate(collection);
|
|
3617
|
-
await
|
|
4331
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3618
4332
|
collection: String(collection),
|
|
3619
4333
|
docs
|
|
3620
4334
|
});
|
|
@@ -3632,7 +4346,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3632
4346
|
async (r) => {
|
|
3633
4347
|
const updateOpts = {
|
|
3634
4348
|
...r.driverOpts,
|
|
3635
|
-
session: r.sdk.session
|
|
4349
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3636
4350
|
};
|
|
3637
4351
|
return coll(collection).updateOne(
|
|
3638
4352
|
toDriverFilter(filter),
|
|
@@ -3643,13 +4357,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3643
4357
|
options
|
|
3644
4358
|
);
|
|
3645
4359
|
invalidate(collection);
|
|
3646
|
-
await
|
|
4360
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
3647
4361
|
collection: String(collection),
|
|
3648
4362
|
filter
|
|
3649
4363
|
});
|
|
3650
4364
|
return result;
|
|
3651
4365
|
};
|
|
3652
4366
|
const updateMany = async (collection, filter, update, options) => {
|
|
4367
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4368
|
+
collection: String(collection),
|
|
4369
|
+
filter
|
|
4370
|
+
});
|
|
3653
4371
|
const effective = stampUpdate(collection, update);
|
|
3654
4372
|
const result = await writeRun(
|
|
3655
4373
|
collection,
|
|
@@ -3657,7 +4375,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3657
4375
|
async (r) => {
|
|
3658
4376
|
const updateOpts = {
|
|
3659
4377
|
...r.driverOpts,
|
|
3660
|
-
session: r.sdk.session
|
|
4378
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3661
4379
|
};
|
|
3662
4380
|
return coll(collection).updateMany(
|
|
3663
4381
|
toDriverFilter(filter),
|
|
@@ -3668,9 +4386,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3668
4386
|
options
|
|
3669
4387
|
);
|
|
3670
4388
|
invalidate(collection);
|
|
4389
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4390
|
+
collection: String(collection),
|
|
4391
|
+
filter
|
|
4392
|
+
});
|
|
3671
4393
|
return result;
|
|
3672
4394
|
};
|
|
3673
4395
|
const findOneAndUpdate = async (collection, filter, update, options) => {
|
|
4396
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4397
|
+
collection: String(collection),
|
|
4398
|
+
filter
|
|
4399
|
+
});
|
|
3674
4400
|
const effective = stampUpdate(collection, update);
|
|
3675
4401
|
const result = await writeRun(
|
|
3676
4402
|
collection,
|
|
@@ -3678,9 +4404,9 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3678
4404
|
async (r) => {
|
|
3679
4405
|
const fOpts = {
|
|
3680
4406
|
...r.driverOpts,
|
|
3681
|
-
session: r.sdk.session,
|
|
3682
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3683
|
-
hint: r.sdk.hint,
|
|
4407
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4408
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4409
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
3684
4410
|
// `after` is the ORM's default, but honor an explicit caller override.
|
|
3685
4411
|
returnDocument: r.driverOpts.returnDocument ?? "after"
|
|
3686
4412
|
};
|
|
@@ -3693,9 +4419,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3693
4419
|
options
|
|
3694
4420
|
);
|
|
3695
4421
|
invalidate(collection);
|
|
4422
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4423
|
+
collection: String(collection),
|
|
4424
|
+
filter
|
|
4425
|
+
});
|
|
3696
4426
|
return result;
|
|
3697
4427
|
};
|
|
3698
4428
|
const findOneAndReplace = async (collection, filter, replacement, options) => {
|
|
4429
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4430
|
+
collection: String(collection),
|
|
4431
|
+
filter
|
|
4432
|
+
});
|
|
3699
4433
|
const effective = stampReplace(collection, replacement);
|
|
3700
4434
|
const result = await writeRun(
|
|
3701
4435
|
collection,
|
|
@@ -3703,9 +4437,9 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3703
4437
|
async (r) => {
|
|
3704
4438
|
const fOpts = {
|
|
3705
4439
|
...r.driverOpts,
|
|
3706
|
-
session: r.sdk.session,
|
|
3707
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3708
|
-
hint: r.sdk.hint,
|
|
4440
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4441
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4442
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
3709
4443
|
// `after` is the ORM's default, but honor an explicit caller override.
|
|
3710
4444
|
returnDocument: r.driverOpts.returnDocument ?? "after"
|
|
3711
4445
|
};
|
|
@@ -3718,16 +4452,24 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3718
4452
|
options
|
|
3719
4453
|
);
|
|
3720
4454
|
invalidate(collection);
|
|
4455
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4456
|
+
collection: String(collection),
|
|
4457
|
+
filter
|
|
4458
|
+
});
|
|
3721
4459
|
return result;
|
|
3722
4460
|
};
|
|
3723
4461
|
const replaceOne = async (collection, filter, replacement, options) => {
|
|
4462
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4463
|
+
collection: String(collection),
|
|
4464
|
+
filter
|
|
4465
|
+
});
|
|
3724
4466
|
const result = await writeRun(
|
|
3725
4467
|
collection,
|
|
3726
4468
|
"mongo.replaceOne",
|
|
3727
4469
|
async (r) => {
|
|
3728
4470
|
const repOpts = {
|
|
3729
4471
|
...r.driverOpts,
|
|
3730
|
-
session: r.sdk.session
|
|
4472
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3731
4473
|
};
|
|
3732
4474
|
return coll(collection).replaceOne(
|
|
3733
4475
|
toDriverFilter(filter),
|
|
@@ -3738,6 +4480,10 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3738
4480
|
options
|
|
3739
4481
|
);
|
|
3740
4482
|
invalidate(collection);
|
|
4483
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4484
|
+
collection: String(collection),
|
|
4485
|
+
filter
|
|
4486
|
+
});
|
|
3741
4487
|
return result;
|
|
3742
4488
|
};
|
|
3743
4489
|
const deleteOne = async (collection, filter, options) => {
|
|
@@ -3751,45 +4497,57 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3751
4497
|
async (r) => {
|
|
3752
4498
|
const delOpts = {
|
|
3753
4499
|
...r.driverOpts,
|
|
3754
|
-
session: r.sdk.session
|
|
4500
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3755
4501
|
};
|
|
3756
|
-
return coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
4502
|
+
return await coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
3757
4503
|
},
|
|
3758
4504
|
options
|
|
3759
4505
|
);
|
|
3760
4506
|
invalidate(collection);
|
|
3761
|
-
await
|
|
4507
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
3762
4508
|
collection: String(collection),
|
|
3763
4509
|
filter
|
|
3764
4510
|
});
|
|
3765
4511
|
return result;
|
|
3766
4512
|
};
|
|
3767
4513
|
const deleteMany = async (collection, filter, options) => {
|
|
4514
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4515
|
+
collection: String(collection),
|
|
4516
|
+
filter
|
|
4517
|
+
});
|
|
3768
4518
|
const result = await writeRun(
|
|
3769
4519
|
collection,
|
|
3770
4520
|
"mongo.deleteMany",
|
|
3771
4521
|
async (r) => {
|
|
3772
4522
|
const delOpts = {
|
|
3773
4523
|
...r.driverOpts,
|
|
3774
|
-
session: r.sdk.session
|
|
4524
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3775
4525
|
};
|
|
3776
|
-
return coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
4526
|
+
return await coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
3777
4527
|
},
|
|
3778
4528
|
options
|
|
3779
4529
|
);
|
|
3780
4530
|
invalidate(collection);
|
|
4531
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4532
|
+
collection: String(collection),
|
|
4533
|
+
filter
|
|
4534
|
+
});
|
|
3781
4535
|
return result;
|
|
3782
4536
|
};
|
|
3783
4537
|
const findOneAndDelete = async (collection, filter, options) => {
|
|
4538
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4539
|
+
collection: String(collection),
|
|
4540
|
+
filter
|
|
4541
|
+
});
|
|
3784
4542
|
const result = await writeRun(
|
|
3785
4543
|
collection,
|
|
3786
4544
|
"mongo.findOneAndDelete",
|
|
3787
4545
|
async (r) => {
|
|
3788
4546
|
const fOpts = {
|
|
3789
4547
|
...r.driverOpts,
|
|
3790
|
-
session: r.sdk.session,
|
|
3791
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
3792
|
-
hint: r.sdk.hint
|
|
4548
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4549
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4550
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3793
4551
|
};
|
|
3794
4552
|
return coll(collection).findOneAndDelete(
|
|
3795
4553
|
toDriverFilter(filter),
|
|
@@ -3799,30 +4557,49 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3799
4557
|
options
|
|
3800
4558
|
);
|
|
3801
4559
|
invalidate(collection);
|
|
4560
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4561
|
+
collection: String(collection),
|
|
4562
|
+
filter
|
|
4563
|
+
});
|
|
3802
4564
|
return result;
|
|
3803
4565
|
};
|
|
3804
4566
|
const softDeleteOne = async (collection, filter, options) => {
|
|
4567
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4568
|
+
collection: String(collection),
|
|
4569
|
+
filter
|
|
4570
|
+
});
|
|
3805
4571
|
const active = mergeMongoActiveFilter(true, filter);
|
|
4572
|
+
const effective = stampUpdate(collection, {
|
|
4573
|
+
$set: { deletedAt: /* @__PURE__ */ new Date() }
|
|
4574
|
+
});
|
|
3806
4575
|
const result = await writeRun(
|
|
3807
4576
|
collection,
|
|
3808
4577
|
"mongo.softDeleteOne",
|
|
3809
4578
|
async (r) => {
|
|
3810
4579
|
const updateOpts = {
|
|
3811
4580
|
...r.driverOpts,
|
|
3812
|
-
session: r.sdk.session
|
|
4581
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3813
4582
|
};
|
|
3814
4583
|
return coll(collection).updateOne(
|
|
3815
4584
|
active,
|
|
3816
|
-
|
|
4585
|
+
effective,
|
|
3817
4586
|
updateOpts
|
|
3818
4587
|
);
|
|
3819
4588
|
},
|
|
3820
4589
|
options
|
|
3821
4590
|
);
|
|
3822
4591
|
invalidate(collection);
|
|
4592
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4593
|
+
collection: String(collection),
|
|
4594
|
+
filter
|
|
4595
|
+
});
|
|
3823
4596
|
return result;
|
|
3824
4597
|
};
|
|
3825
4598
|
const upsert = async (collection, filter, update, options) => {
|
|
4599
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4600
|
+
collection: String(collection),
|
|
4601
|
+
filter
|
|
4602
|
+
});
|
|
3826
4603
|
const formatted = stampUpdate(collection, update);
|
|
3827
4604
|
const result = await writeRun(
|
|
3828
4605
|
collection,
|
|
@@ -3830,17 +4607,26 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3830
4607
|
async (r) => {
|
|
3831
4608
|
const updateOpts = {
|
|
3832
4609
|
...r.driverOpts,
|
|
3833
|
-
session: r.sdk.session,
|
|
4610
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
3834
4611
|
upsert: true
|
|
3835
4612
|
};
|
|
3836
|
-
return coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
4613
|
+
return await coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
3837
4614
|
},
|
|
3838
4615
|
options
|
|
3839
4616
|
);
|
|
3840
4617
|
invalidate(collection);
|
|
4618
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4619
|
+
collection: String(collection),
|
|
4620
|
+
filter
|
|
4621
|
+
});
|
|
3841
4622
|
return result;
|
|
3842
4623
|
};
|
|
3843
4624
|
const bulkUpsert = async (collection, operations, options) => {
|
|
4625
|
+
if (operations.length > MAX_BATCH_OPS) {
|
|
4626
|
+
throw new BadRequest(
|
|
4627
|
+
`bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
3844
4630
|
const writes = operations.map((op) => ({
|
|
3845
4631
|
updateOne: {
|
|
3846
4632
|
filter: op.filter,
|
|
@@ -3848,20 +4634,15 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3848
4634
|
upsert: op.upsert ?? true
|
|
3849
4635
|
}
|
|
3850
4636
|
}));
|
|
3851
|
-
if (operations.length > MAX_BATCH_OPS) {
|
|
3852
|
-
throw new BadRequest(
|
|
3853
|
-
`bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
3854
|
-
);
|
|
3855
|
-
}
|
|
3856
4637
|
const result = await writeRun(
|
|
3857
4638
|
collection,
|
|
3858
4639
|
"mongo.bulkUpsert",
|
|
3859
4640
|
async (r) => {
|
|
3860
4641
|
const bulkOpts = {
|
|
3861
4642
|
...r.driverOpts,
|
|
3862
|
-
session: r.sdk.session
|
|
4643
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3863
4644
|
};
|
|
3864
|
-
return coll(collection).bulkWrite(writes, bulkOpts);
|
|
4645
|
+
return await coll(collection).bulkWrite(writes, bulkOpts);
|
|
3865
4646
|
},
|
|
3866
4647
|
options
|
|
3867
4648
|
);
|
|
@@ -3880,9 +4661,9 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3880
4661
|
async (r) => {
|
|
3881
4662
|
const bulkOpts = {
|
|
3882
4663
|
...r.driverOpts,
|
|
3883
|
-
session: r.sdk.session
|
|
4664
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3884
4665
|
};
|
|
3885
|
-
return coll(collection).bulkWrite(operations, bulkOpts);
|
|
4666
|
+
return await coll(collection).bulkWrite(operations, bulkOpts);
|
|
3886
4667
|
},
|
|
3887
4668
|
options
|
|
3888
4669
|
);
|
|
@@ -3954,14 +4735,29 @@ var tag = (value) => {
|
|
|
3954
4735
|
var untag = (tagged) => {
|
|
3955
4736
|
switch (tagged.t) {
|
|
3956
4737
|
case "oid":
|
|
4738
|
+
if (typeof tagged.v !== "string") {
|
|
4739
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 oid value must be a string");
|
|
4740
|
+
}
|
|
3957
4741
|
return new ObjectId(tagged.v);
|
|
3958
4742
|
case "date":
|
|
4743
|
+
if (typeof tagged.v !== "string") {
|
|
4744
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 date value must be a string");
|
|
4745
|
+
}
|
|
3959
4746
|
return new Date(tagged.v);
|
|
3960
4747
|
case "string":
|
|
4748
|
+
if (typeof tagged.v !== "string") {
|
|
4749
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 string value expected");
|
|
4750
|
+
}
|
|
3961
4751
|
return tagged.v;
|
|
3962
4752
|
case "number":
|
|
4753
|
+
if (typeof tagged.v !== "number") {
|
|
4754
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 number value expected");
|
|
4755
|
+
}
|
|
3963
4756
|
return tagged.v;
|
|
3964
4757
|
case "boolean":
|
|
4758
|
+
if (typeof tagged.v !== "boolean") {
|
|
4759
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 boolean value expected");
|
|
4760
|
+
}
|
|
3965
4761
|
return tagged.v;
|
|
3966
4762
|
case "null":
|
|
3967
4763
|
return null;
|
|
@@ -4018,7 +4814,7 @@ var makePaginateCursor = (ctx) => {
|
|
|
4018
4814
|
throw new BadRequest("paginateCursor: provide either `after` or `before`, not both");
|
|
4019
4815
|
}
|
|
4020
4816
|
const limit = normalizePageLimit(
|
|
4021
|
-
{ page: 1, limit: config.limit },
|
|
4817
|
+
{ page: 1, ...config.limit !== void 0 ? { limit: config.limit } : {} },
|
|
4022
4818
|
config.maxLimit ?? DEFAULT_MAX_LIMIT,
|
|
4023
4819
|
{ limit: DEFAULT_FIND_LIMIT }
|
|
4024
4820
|
).limit;
|
|
@@ -4045,9 +4841,10 @@ var makePaginateCursor = (ctx) => {
|
|
|
4045
4841
|
async (r) => {
|
|
4046
4842
|
const findOpts = {
|
|
4047
4843
|
...r.driverOpts,
|
|
4048
|
-
session: r.sdk.session,
|
|
4049
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
4050
|
-
hint: r.sdk.hint
|
|
4844
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4845
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4846
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
4847
|
+
...r.sdk.batchSize !== void 0 ? { batchSize: r.sdk.batchSize } : {}
|
|
4051
4848
|
};
|
|
4052
4849
|
return client.collection(resolve(String(collection))).find(filter, findOpts).sort(effectiveSort).limit(limit + 1).toArray();
|
|
4053
4850
|
},
|
|
@@ -4060,17 +4857,23 @@ var makePaginateCursor = (ctx) => {
|
|
|
4060
4857
|
logger,
|
|
4061
4858
|
db: dbLabel,
|
|
4062
4859
|
drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
|
|
4063
|
-
getSchema: opts.getSchema
|
|
4860
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
4064
4861
|
},
|
|
4065
4862
|
String(collection),
|
|
4066
4863
|
"mongo.paginateCursor",
|
|
4067
4864
|
page
|
|
4068
4865
|
);
|
|
4069
4866
|
const boundary = backwards ? rows[limit] : page[page.length - 1];
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
values
|
|
4073
|
-
|
|
4867
|
+
let nextCursor = null;
|
|
4868
|
+
if (hasMore && boundary !== void 0) {
|
|
4869
|
+
const values = sortKeys.map(([f]) => boundary[f]);
|
|
4870
|
+
if (values.some((v) => v === void 0)) {
|
|
4871
|
+
throw new BadRequest(
|
|
4872
|
+
"paginateCursor: boundary document is missing a sort field \u2014 every sort key must exist on every document for keyset pagination"
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4875
|
+
nextCursor = encodeCursor({ sort: config.sort, values });
|
|
4876
|
+
}
|
|
4074
4877
|
return { data: page, nextCursor, hasMore };
|
|
4075
4878
|
};
|
|
4076
4879
|
return { paginateCursor };
|
|
@@ -4081,14 +4884,21 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4081
4884
|
const { client, dbLabel, logger, opts, resolve, cachedAggregate } = ctx;
|
|
4082
4885
|
const paginateFlexible = async (collection, baseFilter, config = {}) => {
|
|
4083
4886
|
const normalized = normalizePageLimit(
|
|
4084
|
-
{
|
|
4085
|
-
|
|
4887
|
+
{
|
|
4888
|
+
...config.page !== void 0 ? { page: config.page } : {},
|
|
4889
|
+
...config.limit !== void 0 ? { limit: config.limit } : {}
|
|
4890
|
+
},
|
|
4891
|
+
config.maxLimit ?? DEFAULT_MAX_LIMIT,
|
|
4892
|
+
// Same default as `findMany`/`paginateCursor` — the two pagination
|
|
4893
|
+
// APIs used to disagree silently (10 vs 100).
|
|
4894
|
+
{ limit: DEFAULT_FIND_LIMIT }
|
|
4086
4895
|
);
|
|
4087
4896
|
const dataStages = [
|
|
4088
4897
|
...config.prePipeline ?? [],
|
|
4089
4898
|
...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
|
|
4090
4899
|
{ $skip: normalized.offset },
|
|
4091
4900
|
{ $limit: normalized.limit },
|
|
4901
|
+
...config.projection !== void 0 ? [{ $project: config.projection }] : [],
|
|
4092
4902
|
...config.postPipeline ?? []
|
|
4093
4903
|
];
|
|
4094
4904
|
const pipeline = [
|
|
@@ -4104,30 +4914,32 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4104
4914
|
collection,
|
|
4105
4915
|
opName: "mongo.paginateFlexible",
|
|
4106
4916
|
pipeline,
|
|
4107
|
-
options: config.queryOptions,
|
|
4917
|
+
...config.queryOptions !== void 0 ? { options: config.queryOptions } : {},
|
|
4108
4918
|
execute: async (r) => {
|
|
4109
4919
|
const aggOptions = {
|
|
4110
4920
|
...r.driverOpts,
|
|
4111
|
-
session: r.sdk.session,
|
|
4112
|
-
maxTimeMS: r.sdk.maxTimeMS,
|
|
4113
|
-
hint: r.sdk.hint,
|
|
4114
|
-
batchSize: r.sdk.batchSize
|
|
4921
|
+
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4922
|
+
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4923
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
4924
|
+
...r.sdk.batchSize !== void 0 ? { batchSize: r.sdk.batchSize } : {}
|
|
4115
4925
|
};
|
|
4116
4926
|
const rows = await client.collection(resolve(String(collection))).aggregate(pipeline, aggOptions).toArray();
|
|
4117
4927
|
const row = rows[0];
|
|
4118
4928
|
const data = row?.data ?? [];
|
|
4119
4929
|
const totalCount = row?.totalCount?.[0]?.count ?? 0;
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4930
|
+
if (config.projection === void 0) {
|
|
4931
|
+
checkDocsDrift(
|
|
4932
|
+
{
|
|
4933
|
+
logger,
|
|
4934
|
+
db: dbLabel,
|
|
4935
|
+
drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
|
|
4936
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
4937
|
+
},
|
|
4938
|
+
String(collection),
|
|
4939
|
+
"mongo.paginateFlexible",
|
|
4940
|
+
data
|
|
4941
|
+
);
|
|
4942
|
+
}
|
|
4131
4943
|
return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
|
|
4132
4944
|
}
|
|
4133
4945
|
});
|
|
@@ -4155,9 +4967,12 @@ var makePaginationOps = (client, dbLabel, logger, opts) => {
|
|
|
4155
4967
|
};
|
|
4156
4968
|
|
|
4157
4969
|
// src/service/schema-ops.ts
|
|
4158
|
-
var
|
|
4159
|
-
|
|
4160
|
-
)
|
|
4970
|
+
var optionSignature = (o) => JSON.stringify([
|
|
4971
|
+
Boolean(o?.unique),
|
|
4972
|
+
Boolean(o?.sparse),
|
|
4973
|
+
typeof o?.expireAfterSeconds === "number" ? o.expireAfterSeconds : null
|
|
4974
|
+
]);
|
|
4975
|
+
var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
|
|
4161
4976
|
var makeSchemaOps = (client, _logger, opts) => {
|
|
4162
4977
|
const createSchema = async (collection, createCollectionOptions) => {
|
|
4163
4978
|
const physical = opts.resolveCollectionName(collection);
|
|
@@ -4165,8 +4980,10 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4165
4980
|
const validator = def?.schema ? toMongoValidator(def.schema) : void 0;
|
|
4166
4981
|
try {
|
|
4167
4982
|
await client.createCollection(physical, {
|
|
4168
|
-
|
|
4169
|
-
|
|
4983
|
+
// Caller options FIRST, derived validator LAST — a stale `validator`
|
|
4984
|
+
// key in the options must not clobber the schema-derived one.
|
|
4985
|
+
...createCollectionOptions ?? {},
|
|
4986
|
+
...validator ? { validator } : {}
|
|
4170
4987
|
});
|
|
4171
4988
|
} catch (err) {
|
|
4172
4989
|
if (err?.code === 48) {
|
|
@@ -4211,9 +5028,11 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4211
5028
|
const handle = client.collection(physical);
|
|
4212
5029
|
const existing = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4213
5030
|
const existingByKey = new Map(
|
|
4214
|
-
existing.map((i) => [indexKeyOf(i.key), i.name])
|
|
5031
|
+
existing.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5032
|
+
);
|
|
5033
|
+
const declaredByKey = new Map(
|
|
5034
|
+
declared.map((d) => [indexKeyOf(d.key, d.options), d])
|
|
4215
5035
|
);
|
|
4216
|
-
const declaredByKey = new Map(declared.map((d) => [indexKeyOf(d.key), d]));
|
|
4217
5036
|
const created = [];
|
|
4218
5037
|
const dropped = [];
|
|
4219
5038
|
for (const [key] of declaredByKey) {
|
|
@@ -4225,7 +5044,9 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4225
5044
|
}
|
|
4226
5045
|
}
|
|
4227
5046
|
const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4228
|
-
const afterByKey = new Map(
|
|
5047
|
+
const afterByKey = new Map(
|
|
5048
|
+
after.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5049
|
+
);
|
|
4229
5050
|
for (const [key, spec] of declaredByKey) {
|
|
4230
5051
|
if (!existingByKey.has(key)) {
|
|
4231
5052
|
created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
|
|
@@ -4244,16 +5065,22 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4244
5065
|
|
|
4245
5066
|
// src/service/transaction.ts
|
|
4246
5067
|
var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
5068
|
+
let warnedUnsupported = false;
|
|
5069
|
+
let journalIndexReady = false;
|
|
5070
|
+
const MIGRATE_LEASE_MS = 12e4;
|
|
4247
5071
|
const transaction = async (fn, options) => {
|
|
4248
5072
|
const session = client.startSession();
|
|
4249
5073
|
try {
|
|
4250
5074
|
return await session.withTransaction(async () => fn(session), options);
|
|
4251
5075
|
} catch (err) {
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
5076
|
+
if (isTransactionUnsupportedError(err)) {
|
|
5077
|
+
if (!warnedUnsupported) {
|
|
5078
|
+
warnedUnsupported = true;
|
|
5079
|
+
logger.warn?.(
|
|
5080
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
5081
|
+
"transactions unsupported on this deployment, falling back to non-transactional"
|
|
5082
|
+
);
|
|
5083
|
+
}
|
|
4257
5084
|
return fn(null);
|
|
4258
5085
|
}
|
|
4259
5086
|
if (opts.wrapMongoErrors) {
|
|
@@ -4265,34 +5092,105 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
4265
5092
|
}
|
|
4266
5093
|
};
|
|
4267
5094
|
const migrate = async (name, fn, filePath) => {
|
|
4268
|
-
const journal = handle.collection(
|
|
4269
|
-
|
|
4270
|
-
|
|
5095
|
+
const journal = handle.collection(opts.migrationCollectionPhysical);
|
|
5096
|
+
if (!journalIndexReady) {
|
|
5097
|
+
try {
|
|
5098
|
+
await journal.createIndex({ name: 1 }, { unique: true });
|
|
5099
|
+
} catch {
|
|
5100
|
+
}
|
|
5101
|
+
journalIndexReady = true;
|
|
5102
|
+
}
|
|
5103
|
+
const isDupKeyError = (err) => {
|
|
5104
|
+
const code = err?.code;
|
|
5105
|
+
if (code === 11e3 || code === 11001) return true;
|
|
5106
|
+
return /E11000 duplicate key/.test(String(err?.message ?? ""));
|
|
5107
|
+
};
|
|
4271
5108
|
await transaction(async (session) => {
|
|
4272
5109
|
const sessionOpts = session ? { session } : {};
|
|
4273
|
-
const
|
|
4274
|
-
|
|
5110
|
+
const now = /* @__PURE__ */ new Date();
|
|
5111
|
+
const expiresAt = new Date(Date.now() + MIGRATE_LEASE_MS);
|
|
5112
|
+
const stole = await journal.findOneAndUpdate(
|
|
4275
5113
|
{
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
5114
|
+
name,
|
|
5115
|
+
status: { $exists: true, $nin: ["applied"] },
|
|
5116
|
+
$or: [
|
|
5117
|
+
{ expiresAt: { $lt: now } },
|
|
5118
|
+
{
|
|
5119
|
+
expiresAt: { $exists: false },
|
|
5120
|
+
appliedAt: { $lt: new Date(now.getTime() - MIGRATE_LEASE_MS) }
|
|
5121
|
+
}
|
|
5122
|
+
]
|
|
5123
|
+
},
|
|
5124
|
+
{
|
|
5125
|
+
$set: {
|
|
5126
|
+
status: "running",
|
|
5127
|
+
appliedAt: now,
|
|
5128
|
+
expiresAt,
|
|
4279
5129
|
...filePath ? { file: filePath } : {}
|
|
4280
5130
|
}
|
|
4281
5131
|
},
|
|
4282
|
-
{
|
|
5132
|
+
{ returnDocument: "before", ...sessionOpts }
|
|
4283
5133
|
);
|
|
4284
|
-
if (
|
|
4285
|
-
|
|
5134
|
+
if (stole !== null) {
|
|
5135
|
+
await runClaimed(session);
|
|
4286
5136
|
return;
|
|
4287
5137
|
}
|
|
5138
|
+
try {
|
|
5139
|
+
await journal.insertOne(
|
|
5140
|
+
{
|
|
5141
|
+
name,
|
|
5142
|
+
appliedAt: now,
|
|
5143
|
+
status: "running",
|
|
5144
|
+
expiresAt,
|
|
5145
|
+
...filePath ? { file: filePath } : {}
|
|
5146
|
+
},
|
|
5147
|
+
sessionOpts
|
|
5148
|
+
);
|
|
5149
|
+
} catch (err) {
|
|
5150
|
+
if (!isDupKeyError(err)) throw err;
|
|
5151
|
+
const existing = await journal.findOne(
|
|
5152
|
+
{ name },
|
|
5153
|
+
{ projection: { status: 1 }, ...sessionOpts }
|
|
5154
|
+
);
|
|
5155
|
+
const status = existing?.status;
|
|
5156
|
+
if (status === void 0 || status === "applied") {
|
|
5157
|
+
logger.info({ name }, "migration already applied, skipping");
|
|
5158
|
+
} else {
|
|
5159
|
+
logger.warn({ name }, "migration claim held by another runner, skipping");
|
|
5160
|
+
}
|
|
5161
|
+
return;
|
|
5162
|
+
}
|
|
5163
|
+
await runClaimed(session);
|
|
5164
|
+
});
|
|
5165
|
+
async function runClaimed(session) {
|
|
5166
|
+
const sessionOpts = session ? { session } : {};
|
|
5167
|
+
const heartbeat = session === null ? setInterval(() => {
|
|
5168
|
+
void journal.updateOne(
|
|
5169
|
+
{ name, status: "running" },
|
|
5170
|
+
{ $set: { expiresAt: new Date(Date.now() + MIGRATE_LEASE_MS) } }
|
|
5171
|
+
).catch(() => {
|
|
5172
|
+
});
|
|
5173
|
+
}, MIGRATE_LEASE_MS / 2) : void 0;
|
|
5174
|
+
heartbeat?.unref?.();
|
|
4288
5175
|
try {
|
|
4289
5176
|
await fn(session);
|
|
5177
|
+
const res = await journal.updateOne(
|
|
5178
|
+
{ name, status: "running" },
|
|
5179
|
+
{ $set: { status: "applied" } },
|
|
5180
|
+
sessionOpts
|
|
5181
|
+
);
|
|
5182
|
+
if (res?.matchedCount === 0) {
|
|
5183
|
+
await journal.insertOne({ name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }, sessionOpts).catch(() => {
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
4290
5186
|
} catch (err) {
|
|
4291
5187
|
await journal.deleteOne({ name }, sessionOpts).catch(() => {
|
|
4292
5188
|
});
|
|
4293
5189
|
throw err;
|
|
5190
|
+
} finally {
|
|
5191
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
4294
5192
|
}
|
|
4295
|
-
}
|
|
5193
|
+
}
|
|
4296
5194
|
};
|
|
4297
5195
|
return { transaction, migrate };
|
|
4298
5196
|
};
|
|
@@ -4323,27 +5221,27 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
4323
5221
|
const crud = makeCrudOps(handle, dbName, logger, {
|
|
4324
5222
|
resolveCollectionName,
|
|
4325
5223
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
4326
|
-
cache: sharedCache,
|
|
5224
|
+
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
4327
5225
|
dedupeReads,
|
|
4328
5226
|
inFlight,
|
|
4329
5227
|
hooks,
|
|
4330
5228
|
timestamps,
|
|
4331
|
-
drift: deps2.drift,
|
|
4332
|
-
getSchema: (logical) => getDefinition(logical)?.schema
|
|
5229
|
+
...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
|
|
5230
|
+
...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {}
|
|
4333
5231
|
});
|
|
4334
5232
|
const pagination = makePaginationOps(handle, dbName, logger, {
|
|
4335
5233
|
resolveCollectionName,
|
|
4336
5234
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
4337
|
-
drift: deps2.drift,
|
|
4338
|
-
getSchema: (logical) => getDefinition(logical)?.schema,
|
|
4339
|
-
cache: sharedCache,
|
|
5235
|
+
...deps2.drift !== void 0 ? { drift: deps2.drift } : {},
|
|
5236
|
+
...getDefinition !== void 0 ? { getSchema: (logical) => getDefinition(logical)?.schema } : {},
|
|
5237
|
+
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
4340
5238
|
dedupeReads,
|
|
4341
5239
|
inFlight
|
|
4342
5240
|
});
|
|
4343
5241
|
const aggregation = makeAggregationOps(handle, dbName, logger, {
|
|
4344
5242
|
resolveCollectionName,
|
|
4345
5243
|
wrapMongoErrors: deps2.wrapMongoErrors,
|
|
4346
|
-
cache: sharedCache,
|
|
5244
|
+
...sharedCache !== void 0 ? { cache: sharedCache } : {},
|
|
4347
5245
|
dedupeReads,
|
|
4348
5246
|
inFlight
|
|
4349
5247
|
});
|
|
@@ -4356,7 +5254,7 @@ var buildManager = (deps2, dbKey, dbName, client) => {
|
|
|
4356
5254
|
findMany: async (logical, filter, options) => {
|
|
4357
5255
|
const physical = resolveCollectionName(logical);
|
|
4358
5256
|
return handle.collection(physical).find(filter, {
|
|
4359
|
-
projection: options
|
|
5257
|
+
...options?.projection !== void 0 ? { projection: options.projection } : {},
|
|
4360
5258
|
limit: options?.limit ?? 1e4
|
|
4361
5259
|
}).toArray();
|
|
4362
5260
|
}
|
|
@@ -4396,11 +5294,11 @@ var createMongoService = (dbClients, configOverrides = {}) => {
|
|
|
4396
5294
|
dbClients,
|
|
4397
5295
|
config,
|
|
4398
5296
|
logger,
|
|
4399
|
-
sharedCache,
|
|
5297
|
+
...sharedCache !== void 0 ? { sharedCache } : {},
|
|
4400
5298
|
dedupeReads,
|
|
4401
5299
|
inFlight,
|
|
4402
5300
|
wrapMongoErrors: runtime.wrapMongoErrors,
|
|
4403
|
-
drift: configOverrides.drift,
|
|
5301
|
+
...configOverrides.drift !== void 0 ? { drift: configOverrides.drift } : {},
|
|
4404
5302
|
collectionRefs,
|
|
4405
5303
|
db
|
|
4406
5304
|
},
|
|
@@ -4437,11 +5335,27 @@ var createMongoService = (dbClients, configOverrides = {}) => {
|
|
|
4437
5335
|
};
|
|
4438
5336
|
|
|
4439
5337
|
// src/shared/merge-filters.ts
|
|
5338
|
+
var isOperatorDoc = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4440
5339
|
var mergeMongoFilters = (...parts) => {
|
|
4441
5340
|
const out = {};
|
|
4442
5341
|
for (const part of parts) {
|
|
4443
5342
|
if (!part) continue;
|
|
4444
|
-
Object.
|
|
5343
|
+
for (const [key, value] of Object.entries(part)) {
|
|
5344
|
+
if (!(key in out)) {
|
|
5345
|
+
out[key] = value;
|
|
5346
|
+
continue;
|
|
5347
|
+
}
|
|
5348
|
+
if (key === "$and" || key === "$or") {
|
|
5349
|
+
const prev2 = out[key];
|
|
5350
|
+
out[key] = [
|
|
5351
|
+
...Array.isArray(prev2) ? prev2 : [prev2],
|
|
5352
|
+
...Array.isArray(value) ? value : [value]
|
|
5353
|
+
];
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
const prev = out[key];
|
|
5357
|
+
out[key] = isOperatorDoc(prev) && isOperatorDoc(value) ? { ...prev, ...value } : value;
|
|
5358
|
+
}
|
|
4445
5359
|
}
|
|
4446
5360
|
return out;
|
|
4447
5361
|
};
|
|
@@ -4520,6 +5434,6 @@ var createCachedAsyncFactory = (factory, options = {}) => {
|
|
|
4520
5434
|
};
|
|
4521
5435
|
};
|
|
4522
5436
|
|
|
4523
|
-
export { AppError, BadRequest, CacheInvalidator, DEFAULT_FIND_LIMIT, DEFAULT_MAX_LIMIT, DataLoader, DomainError, ERROR_HTTP_STATUS, HOOK_NAMES, HotCache, InFlight, InfraError, LRU, MAX_BATCH_OPS, QueryBuilder, QueryCache, TRANSIENT_MONGO_ERROR_CODES, belongsTo, buildKeysetFilter, buildMongoActiveFilter, buildPaginationResult, cacheCollectionKey, canonicalKey, createCachedAsyncFactory, createCachedFactory, createConsoleLogger, createHotCache, createMongoCapabilitiesStore, createMongoMigrationRunner, createMongoService, createMongoToolkit, createNoopLogger, createRepository, createResolveCollectionName, decodeCursor, defineCollection, defineCollections, encodeCursor, hasMany, hashString, httpStatusForError, isAppError, isDomainError, isInfraError, isMappedMongoError, isMongoDuplicateKeyError, isMongoTransientError, manyToMany, mapMongoDriverError, mergeMongoActiveFilter, mergeMongoFilters, normalizeMongoUrl, normalizePageLimit, optional, probeMongoCapabilities, runHooks, s, serializeError, sleep, sleepJittered, stableHash, stableStringify, stripDocumentId, stripPrimaryKey, toMongoSchema, toMongoValidator, validateDoc, withDefault, withGracefulMongoTransaction, withRetry, withTimeout, withTransaction };
|
|
5437
|
+
export { AppError, BadRequest, CacheInvalidator, DEFAULT_FIND_LIMIT, DEFAULT_MAX_LIMIT, DataLoader, DomainError, ERROR_HTTP_STATUS, HOOK_NAMES, HotCache, InFlight, InfraError, LRU, MAX_BATCH_OPS, QueryBuilder, QueryCache, TRANSIENT_MONGO_ERROR_CODES, belongsTo, buildKeysetFilter, buildMongoActiveFilter, buildPaginationResult, cacheCollectionKey, canonicalKey, createCachedAsyncFactory, createCachedFactory, createConsoleLogger, createHotCache, createMongoCapabilitiesStore, createMongoMigrationRunner, createMongoService, createMongoToolkit, createNoopLogger, createRepository, createResolveCollectionName, decodeCursor, defineCollection, defineCollections, encodeCursor, hasMany, hashString, httpStatusForError, isAppError, isDomainError, isInfraError, isMappedMongoError, isMongoDuplicateKeyError, isMongoTransientError, isTransactionUnsupportedError, manyToMany, mapMongoDriverError, mergeMongoActiveFilter, mergeMongoFilters, normalizeMongoUrl, normalizePageLimit, optional, probeMongoCapabilities, runHooks, s, serializeError, sleep, sleepJittered, stableHash, stableStringify, stripDocumentId, stripPrimaryKey, toMongoSchema, toMongoValidator, validateDoc, withDefault, withGracefulMongoTransaction, withRetry, withTimeout, withTransaction };
|
|
4524
5438
|
//# sourceMappingURL=index.js.map
|
|
4525
5439
|
//# sourceMappingURL=index.js.map
|