@ignex/ninox 0.1.4 → 0.1.6
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 +60 -10
- package/dist/index.d.ts +221 -70
- package/dist/index.js +1314 -386
- package/dist/index.js.map +1 -1
- package/dist/{timeout-BE_ULjdG.d.ts → timeout-B10CTiGm.d.ts} +13 -1
- package/dist/utils.d.ts +12 -7
- package/dist/utils.js +63 -8
- package/dist/utils.js.map +1 -1
- package/package.json +1 -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';
|
|
@@ -56,14 +56,6 @@ var probeMongoCapabilities = async (client) => {
|
|
|
56
56
|
return { probed: false, transactionsSupported: false };
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
|
-
var mongoTransactionsEnabled = (store, options = {}) => {
|
|
60
|
-
const env = options.envOverride ?? readMongoTransactionsEnvOverride();
|
|
61
|
-
if (env !== void 0) return env;
|
|
62
|
-
const state = store.get();
|
|
63
|
-
if (state.probed) return state.transactionsSupported;
|
|
64
|
-
if (options.urlHint) return /replicaSet=/.test(options.urlHint);
|
|
65
|
-
return false;
|
|
66
|
-
};
|
|
67
59
|
|
|
68
60
|
// src/errors/classes.ts
|
|
69
61
|
var ERROR_HTTP_STATUS = {
|
|
@@ -138,8 +130,14 @@ var TRANSIENT_MONGO_ERROR_CODES = Object.freeze(
|
|
|
138
130
|
// ShutdownInProgress
|
|
139
131
|
189,
|
|
140
132
|
// PrimarySteppedDown
|
|
133
|
+
251,
|
|
134
|
+
// NoSuchTransaction (stepdown mid-transaction)
|
|
135
|
+
262,
|
|
136
|
+
// ExceededTimeLimit (part of the retryable transaction set)
|
|
141
137
|
9001,
|
|
142
138
|
// SocketException
|
|
139
|
+
10058,
|
|
140
|
+
// NotWritablePrimary (legacy server code)
|
|
143
141
|
10107,
|
|
144
142
|
// NotWritablePrimary
|
|
145
143
|
11600,
|
|
@@ -152,10 +150,13 @@ var TRANSIENT_MONGO_ERROR_CODES = Object.freeze(
|
|
|
152
150
|
// NotPrimaryOrSecondary
|
|
153
151
|
])
|
|
154
152
|
);
|
|
155
|
-
var TRANSIENT_NAME = /MongoNetworkError|PoolClearedError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoNetworkTimeoutError/;
|
|
153
|
+
var TRANSIENT_NAME = /MongoNetworkError|PoolClearedError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoNetworkTimeoutError|MongoServerSelectionError/;
|
|
156
154
|
var isMongoTransientError = (error) => {
|
|
157
155
|
if (!error || typeof error !== "object") return false;
|
|
158
156
|
const e = error;
|
|
157
|
+
if (typeof e.hasErrorLabel === "function" && e.hasErrorLabel("TransientTransactionError")) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
159
160
|
if (typeof e.name === "string" && TRANSIENT_NAME.test(e.name)) return true;
|
|
160
161
|
if (typeof e.code === "number" && TRANSIENT_MONGO_ERROR_CODES.has(e.code)) return true;
|
|
161
162
|
return false;
|
|
@@ -167,6 +168,42 @@ var isMongoDuplicateKeyError = (error) => {
|
|
|
167
168
|
if (typeof e.message === "string" && /E11000|duplicate key/i.test(e.message)) return true;
|
|
168
169
|
return false;
|
|
169
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
|
+
};
|
|
170
207
|
|
|
171
208
|
// src/errors/driver-map.ts
|
|
172
209
|
var extractValidationPaths = (errInfo) => {
|
|
@@ -195,6 +232,7 @@ var addValidationContext = (context, errInfo) => {
|
|
|
195
232
|
var isBulkWriteError = (error) => typeof error === "object" && error !== null && "writeErrors" in error && Array.isArray(error.writeErrors);
|
|
196
233
|
var mapMongoDriverError = (error, ctx = {}) => {
|
|
197
234
|
if (error instanceof AppError) return error;
|
|
235
|
+
if (!isMongoDriverError(error)) return error;
|
|
198
236
|
const context = { ...ctx };
|
|
199
237
|
if (isMongoDuplicateKeyError(error)) {
|
|
200
238
|
const e = error;
|
|
@@ -207,7 +245,7 @@ var mapMongoDriverError = (error, ctx = {}) => {
|
|
|
207
245
|
if (first?.code === 11e3) {
|
|
208
246
|
if (first.keyPattern) context.keyPattern = first.keyPattern;
|
|
209
247
|
if (first.keyValue) context.keyValue = first.keyValue;
|
|
210
|
-
return new DomainError("DUPLICATE_KEY",
|
|
248
|
+
return new DomainError("DUPLICATE_KEY", "Duplicate key error", context);
|
|
211
249
|
}
|
|
212
250
|
if (first?.code === 121) {
|
|
213
251
|
addValidationContext(context, first.errInfo);
|
|
@@ -252,7 +290,47 @@ var serializeError = (err, ctx = {}) => {
|
|
|
252
290
|
var cloneDeep = (value) => {
|
|
253
291
|
if (value instanceof Date) return new Date(value.getTime());
|
|
254
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
|
+
}
|
|
255
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
|
+
}
|
|
256
334
|
if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
|
|
257
335
|
if (value !== null && typeof value === "object") {
|
|
258
336
|
const out = {};
|
|
@@ -265,7 +343,10 @@ var cloneDeep = (value) => {
|
|
|
265
343
|
};
|
|
266
344
|
|
|
267
345
|
// src/utils/hash.ts
|
|
268
|
-
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";
|
|
269
350
|
var toPrimitive = (v) => {
|
|
270
351
|
if (v === void 0 || v === null) return null;
|
|
271
352
|
if (typeof v === "bigint") return v.toString();
|
|
@@ -290,11 +371,15 @@ var stableStringify = (value) => {
|
|
|
290
371
|
return JSON.stringify(visit(value));
|
|
291
372
|
};
|
|
292
373
|
var hashString = (input) => {
|
|
293
|
-
let
|
|
374
|
+
let h1 = 5381;
|
|
375
|
+
let h2 = 2166136261;
|
|
294
376
|
for (let i = 0; i < input.length; i++) {
|
|
295
|
-
|
|
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;
|
|
296
381
|
}
|
|
297
|
-
return (
|
|
382
|
+
return `${(h1 >>> 0).toString(36)}${(h2 >>> 0).toString(36)}`;
|
|
298
383
|
};
|
|
299
384
|
var stableHash = (value) => hashString(stableStringify(value));
|
|
300
385
|
|
|
@@ -302,11 +387,13 @@ var stableHash = (value) => hashString(stableStringify(value));
|
|
|
302
387
|
var LRU = class {
|
|
303
388
|
max;
|
|
304
389
|
map = /* @__PURE__ */ new Map();
|
|
390
|
+
onEvict;
|
|
305
391
|
/** Number of entries evicted to make room for new ones (capacity evictions only). */
|
|
306
392
|
evictions = 0;
|
|
307
393
|
constructor(options) {
|
|
308
394
|
if (options.max <= 0) throw new Error("LRU max must be > 0");
|
|
309
395
|
this.max = options.max;
|
|
396
|
+
this.onEvict = options.onEvict;
|
|
310
397
|
}
|
|
311
398
|
get size() {
|
|
312
399
|
return this.map.size;
|
|
@@ -328,10 +415,16 @@ var LRU = class {
|
|
|
328
415
|
if (this.map.has(key)) this.map.delete(key);
|
|
329
416
|
this.map.set(key, value);
|
|
330
417
|
if (this.map.size > this.max) {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
|
|
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);
|
|
334
423
|
this.evictions++;
|
|
424
|
+
try {
|
|
425
|
+
this.onEvict?.(evictedKey, evictedValue);
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
335
428
|
}
|
|
336
429
|
}
|
|
337
430
|
return this;
|
|
@@ -362,9 +455,20 @@ var InFlight = class {
|
|
|
362
455
|
run(key, fn) {
|
|
363
456
|
const existing = this.pending.get(key);
|
|
364
457
|
if (existing) return existing;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
);
|
|
368
472
|
this.pending.set(key, promise);
|
|
369
473
|
return promise;
|
|
370
474
|
}
|
|
@@ -372,24 +476,50 @@ var InFlight = class {
|
|
|
372
476
|
this.pending.clear();
|
|
373
477
|
}
|
|
374
478
|
};
|
|
375
|
-
var
|
|
479
|
+
var OVERHEAD_PER_KEY = 8;
|
|
480
|
+
var estimateSize = (value, seen) => {
|
|
376
481
|
if (value === null || value === void 0) return 0;
|
|
377
482
|
if (typeof value === "string") return value.length * 2;
|
|
378
483
|
if (typeof value === "number" || typeof value === "boolean") return 8;
|
|
484
|
+
if (typeof value === "bigint") return 16;
|
|
485
|
+
if (typeof value !== "object") return 0;
|
|
379
486
|
if (value instanceof ObjectId) return 16;
|
|
380
487
|
if (value instanceof Date) return 8;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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);
|
|
386
493
|
}
|
|
387
|
-
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;
|
|
500
|
+
}
|
|
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);
|
|
388
517
|
}
|
|
389
|
-
return
|
|
518
|
+
return sum;
|
|
390
519
|
};
|
|
391
520
|
|
|
392
521
|
// src/cache/hot-cache/ticker.ts
|
|
522
|
+
var MAX_TICK_INFLIGHT = 64;
|
|
393
523
|
var RefreshTicker = class {
|
|
394
524
|
constructor(host, tickIntervalMs) {
|
|
395
525
|
this.host = host;
|
|
@@ -424,17 +554,26 @@ var RefreshTicker = class {
|
|
|
424
554
|
this.timer = void 0;
|
|
425
555
|
}
|
|
426
556
|
}
|
|
427
|
-
/**
|
|
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
|
+
*/
|
|
428
563
|
tick() {
|
|
429
564
|
const now = Date.now();
|
|
565
|
+
let budget = MAX_TICK_INFLIGHT;
|
|
430
566
|
for (const q of this.host.queries().values()) {
|
|
431
567
|
if (q.refreshIntervalMs <= 0) continue;
|
|
432
568
|
for (const [key, entry] of [...q.lru.entries()]) {
|
|
433
569
|
if (entry.nextRefreshAt <= now) {
|
|
570
|
+
if (budget <= 0) return;
|
|
571
|
+
budget--;
|
|
572
|
+
entry.nextRefreshAt = now + q.refreshIntervalMs;
|
|
434
573
|
q.refreshes++;
|
|
435
574
|
void this.host.fetch(q, key, entry.args, q.gen).catch((err) => {
|
|
436
575
|
const current = q.lru.peek(key);
|
|
437
|
-
if (current) current.nextRefreshAt = now
|
|
576
|
+
if (current) current.nextRefreshAt = Date.now();
|
|
438
577
|
this.host.logger.warn?.(
|
|
439
578
|
{
|
|
440
579
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -468,6 +607,15 @@ var RefreshTicker = class {
|
|
|
468
607
|
|
|
469
608
|
// src/cache/hot-cache/types.ts
|
|
470
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
|
+
};
|
|
471
619
|
|
|
472
620
|
// src/utils/watch-errors.ts
|
|
473
621
|
var isPermanentWatchError = (message) => (
|
|
@@ -482,6 +630,21 @@ var isPermanentWatchError = (message) => (
|
|
|
482
630
|
);
|
|
483
631
|
|
|
484
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
|
+
};
|
|
485
648
|
var WatchCoordinator = class {
|
|
486
649
|
constructor(host) {
|
|
487
650
|
this.host = host;
|
|
@@ -489,6 +652,8 @@ var WatchCoordinator = class {
|
|
|
489
652
|
streams = /* @__PURE__ */ new Map();
|
|
490
653
|
failedStreams = /* @__PURE__ */ new Set();
|
|
491
654
|
stopped = true;
|
|
655
|
+
/** Guards against stacking reconnect timers when several accessors lag. */
|
|
656
|
+
reopenTimer;
|
|
492
657
|
/** Number of currently open change streams. */
|
|
493
658
|
get size() {
|
|
494
659
|
return this.streams.size;
|
|
@@ -501,6 +666,7 @@ var WatchCoordinator = class {
|
|
|
501
666
|
/** Close every watcher and clear failure bookkeeping (idempotent). */
|
|
502
667
|
async stop() {
|
|
503
668
|
this.stopped = true;
|
|
669
|
+
if (this.reopenTimer) clearTimeout(this.reopenTimer);
|
|
504
670
|
const closes = [...this.streams.values()].map((stream) => {
|
|
505
671
|
try {
|
|
506
672
|
return Promise.resolve(stream.close()).catch(() => {
|
|
@@ -515,14 +681,32 @@ var WatchCoordinator = class {
|
|
|
515
681
|
}
|
|
516
682
|
openWatchStreams() {
|
|
517
683
|
const seen = /* @__PURE__ */ new Set();
|
|
684
|
+
let deferred = false;
|
|
518
685
|
for (const q of this.host.queries().values()) {
|
|
519
686
|
for (const ref of q.config.watch ?? []) {
|
|
520
|
-
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}`;
|
|
521
693
|
if (seen.has(key) || this.streams.has(key)) continue;
|
|
522
694
|
seen.add(key);
|
|
523
|
-
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
|
+
});
|
|
524
701
|
}
|
|
525
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
|
+
}
|
|
526
710
|
}
|
|
527
711
|
/**
|
|
528
712
|
* Long-lived change-stream consumer for one collection. Each data change
|
|
@@ -536,11 +720,19 @@ var WatchCoordinator = class {
|
|
|
536
720
|
async watchLoop(key, ref) {
|
|
537
721
|
let backoff = 0;
|
|
538
722
|
while (!this.stopped) {
|
|
723
|
+
let connectedAt = Date.now();
|
|
539
724
|
try {
|
|
540
|
-
const
|
|
541
|
-
|
|
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
|
+
);
|
|
542
733
|
});
|
|
543
734
|
this.streams.set(key, stream);
|
|
735
|
+
connectedAt = Date.now();
|
|
544
736
|
if (backoff > 0) {
|
|
545
737
|
this.host.logger.warn?.(
|
|
546
738
|
{ collection: ref.collection },
|
|
@@ -549,10 +741,13 @@ var WatchCoordinator = class {
|
|
|
549
741
|
this.host.invalidateCollection(ref.collection);
|
|
550
742
|
}
|
|
551
743
|
const iterator = stream[Symbol.asyncIterator]();
|
|
552
|
-
for await (const
|
|
553
|
-
|
|
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);
|
|
554
748
|
}
|
|
555
749
|
} catch (err) {
|
|
750
|
+
if (Date.now() - connectedAt > 6e4) backoff = 0;
|
|
556
751
|
const message = err instanceof Error ? err.message : String(err);
|
|
557
752
|
this.host.logger.warn?.(
|
|
558
753
|
{ error: message, collection: ref.collection },
|
|
@@ -589,7 +784,9 @@ var WatchCoordinator = class {
|
|
|
589
784
|
const set = /* @__PURE__ */ new Set();
|
|
590
785
|
for (const q of this.host.queries().values()) {
|
|
591
786
|
for (const ref of q.config.watch ?? []) {
|
|
592
|
-
|
|
787
|
+
const db = resolveDbSafe(ref, this.host.logger);
|
|
788
|
+
if (!db) continue;
|
|
789
|
+
set.add(`${db.databaseName}${WATCH_SEP}${ref.collection}`);
|
|
593
790
|
}
|
|
594
791
|
}
|
|
595
792
|
return set;
|
|
@@ -630,10 +827,14 @@ var HotCache = class {
|
|
|
630
827
|
this.watcher = new WatchCoordinator({
|
|
631
828
|
queries: () => this.queries,
|
|
632
829
|
invalidateCollection: (collection) => this.invalidateCollection(collection),
|
|
830
|
+
invalidateDocument: (collection, id) => this.invalidateIds(collection, [id]),
|
|
633
831
|
isReplica: () => this._mode === "replica",
|
|
634
832
|
fallbackToStandalone: () => {
|
|
635
833
|
this._mode = "standalone";
|
|
636
|
-
if (this.refreshEnabled
|
|
834
|
+
if (this.refreshEnabled && !this.ticker.running) {
|
|
835
|
+
this.ticker.start();
|
|
836
|
+
this.ticker.warnUnboundedStaleness();
|
|
837
|
+
}
|
|
637
838
|
},
|
|
638
839
|
logger: this.logger
|
|
639
840
|
});
|
|
@@ -676,25 +877,26 @@ var HotCache = class {
|
|
|
676
877
|
refreshIntervalMs: config.refreshIntervalMs ?? 0,
|
|
677
878
|
clone: config.clone ?? false,
|
|
678
879
|
...config.maxValueBytes !== void 0 ? { maxValueBytes: config.maxValueBytes } : {},
|
|
880
|
+
hasIdWatch: (config.watch ?? []).some((ref) => typeof ref.idsOf === "function"),
|
|
679
881
|
gen: 0,
|
|
680
882
|
hits: 0,
|
|
681
883
|
misses: 0,
|
|
682
884
|
refreshes: 0,
|
|
683
885
|
loadErrors: 0,
|
|
684
886
|
sizeSkips: 0,
|
|
887
|
+
idDrops: 0,
|
|
685
888
|
warnedSize: false
|
|
686
889
|
};
|
|
687
890
|
this.queries.set(name, query);
|
|
688
891
|
if (this._mode === "replica" && !this.stopped) {
|
|
689
892
|
this.watcher.start();
|
|
690
893
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
else this.invalidateParams(name, ...args);
|
|
696
|
-
}
|
|
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);
|
|
697
898
|
};
|
|
899
|
+
return { get, invalidate };
|
|
698
900
|
}
|
|
699
901
|
/**
|
|
700
902
|
* Read-through read by dynamic name lookup. Call parameters are hashed into
|
|
@@ -716,7 +918,7 @@ var HotCache = class {
|
|
|
716
918
|
q.lru.delete(key);
|
|
717
919
|
}
|
|
718
920
|
q.misses++;
|
|
719
|
-
return this.fetch(q, key, args, q.gen);
|
|
921
|
+
return await this.fetch(q, key, args, q.gen);
|
|
720
922
|
}
|
|
721
923
|
/** Drop every cached entry for a registered query. */
|
|
722
924
|
invalidate(name) {
|
|
@@ -749,6 +951,48 @@ var HotCache = class {
|
|
|
749
951
|
}
|
|
750
952
|
}
|
|
751
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
|
+
}
|
|
752
996
|
/** Drop every cached entry for every query. */
|
|
753
997
|
clear() {
|
|
754
998
|
for (const q of this.queries.values()) {
|
|
@@ -758,9 +1002,10 @@ var HotCache = class {
|
|
|
758
1002
|
}
|
|
759
1003
|
/**
|
|
760
1004
|
* Runtime observability snapshot: total queries/entries plus per-query
|
|
761
|
-
* counters (`hits`, `misses`, background `refreshes`, `loadErrors`,
|
|
762
|
-
* `sizeSkips` for entries dropped by `maxValueBytes`
|
|
763
|
-
*
|
|
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.
|
|
764
1009
|
*/
|
|
765
1010
|
stats() {
|
|
766
1011
|
const perQuery = {};
|
|
@@ -772,6 +1017,7 @@ var HotCache = class {
|
|
|
772
1017
|
refreshes: q.refreshes,
|
|
773
1018
|
loadErrors: q.loadErrors,
|
|
774
1019
|
sizeSkips: q.sizeSkips,
|
|
1020
|
+
idDrops: q.idDrops,
|
|
775
1021
|
evictions: q.lru.evictions
|
|
776
1022
|
};
|
|
777
1023
|
}
|
|
@@ -780,12 +1026,15 @@ var HotCache = class {
|
|
|
780
1026
|
/**
|
|
781
1027
|
* Probe replica support once, then start change-stream watchers (replica) or
|
|
782
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).
|
|
783
1032
|
*/
|
|
784
1033
|
async start() {
|
|
785
1034
|
if (this.disposed) return this._mode;
|
|
786
1035
|
if (this.started) return this._mode;
|
|
787
1036
|
if (!this.startPromise) {
|
|
788
|
-
this.startPromise = this.runStart();
|
|
1037
|
+
this.startPromise = this.runStart().catch(this.forgetFailedStart);
|
|
789
1038
|
}
|
|
790
1039
|
return this.startPromise;
|
|
791
1040
|
}
|
|
@@ -807,6 +1056,17 @@ var HotCache = class {
|
|
|
807
1056
|
this._mode = "unknown";
|
|
808
1057
|
}
|
|
809
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
|
+
};
|
|
810
1070
|
async runStart() {
|
|
811
1071
|
this.stopped = false;
|
|
812
1072
|
const supported = this.options.mode === "replica" ? true : this.options.mode === "standalone" ? false : await (this.options.probe ?? this.defaultProbe)();
|
|
@@ -830,20 +1090,34 @@ var HotCache = class {
|
|
|
830
1090
|
ensureStarted() {
|
|
831
1091
|
if (this.disposed || this.started || this.startAttempted) return;
|
|
832
1092
|
this.startAttempted = true;
|
|
833
|
-
|
|
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;
|
|
834
1097
|
this.logger.warn?.(
|
|
835
|
-
{ error:
|
|
1098
|
+
{ error: wrapped.message, stack: wrapped.stack, trigger: origin.stack },
|
|
836
1099
|
"hot cache start failed; reads continue without auto-refresh"
|
|
837
1100
|
);
|
|
838
|
-
}
|
|
1101
|
+
};
|
|
1102
|
+
this.start().catch(reportStartFailure);
|
|
839
1103
|
}
|
|
840
1104
|
defaultProbe = async () => {
|
|
841
1105
|
for (const q of this.queries.values()) {
|
|
842
1106
|
for (const ref of q.config.watch ?? []) {
|
|
843
|
-
|
|
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);
|
|
844
1118
|
if (!caps.probed) {
|
|
845
1119
|
this.logger.warn?.(
|
|
846
|
-
{ db:
|
|
1120
|
+
{ db: db.databaseName },
|
|
847
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"
|
|
848
1122
|
);
|
|
849
1123
|
return false;
|
|
@@ -854,17 +1128,18 @@ var HotCache = class {
|
|
|
854
1128
|
return false;
|
|
855
1129
|
};
|
|
856
1130
|
fetch(q, key, args, gen) {
|
|
857
|
-
const dedupeKey = `${q.name}${WATCH_SEP}${key}`;
|
|
858
|
-
|
|
1131
|
+
const dedupeKey = `${q.name}${WATCH_SEP}g${gen}${WATCH_SEP}${key}`;
|
|
1132
|
+
const loadOnce = async () => {
|
|
859
1133
|
try {
|
|
860
1134
|
const value = await q.config.loader(...args);
|
|
861
1135
|
if (q.gen === gen) {
|
|
862
|
-
|
|
1136
|
+
const bytes = q.maxValueBytes !== void 0 ? estimateSize(value) : void 0;
|
|
1137
|
+
if (bytes !== void 0 && bytes > q.maxValueBytes) {
|
|
863
1138
|
q.sizeSkips++;
|
|
864
1139
|
if (!q.warnedSize) {
|
|
865
1140
|
q.warnedSize = true;
|
|
866
1141
|
this.logger.warn?.(
|
|
867
|
-
{ query: q.name, bytes
|
|
1142
|
+
{ query: q.name, bytes, max: q.maxValueBytes },
|
|
868
1143
|
"hot cache: value exceeds maxValueBytes \u2014 returned but not cached"
|
|
869
1144
|
);
|
|
870
1145
|
}
|
|
@@ -877,17 +1152,42 @@ var HotCache = class {
|
|
|
877
1152
|
q.loadErrors++;
|
|
878
1153
|
throw err;
|
|
879
1154
|
}
|
|
880
|
-
}
|
|
1155
|
+
};
|
|
1156
|
+
return this.inflight.run(dedupeKey, loadOnce);
|
|
881
1157
|
}
|
|
882
1158
|
setEntry(q, key, args, value) {
|
|
883
1159
|
const now = Date.now();
|
|
1160
|
+
const deps2 = q.hasIdWatch ? this.computeDeps(q, args) : {};
|
|
884
1161
|
q.lru.set(key, {
|
|
885
1162
|
value,
|
|
886
1163
|
args,
|
|
887
1164
|
expiresAt: q.ttlMs > 0 ? now + q.ttlMs : Infinity,
|
|
888
|
-
nextRefreshAt: q.refreshIntervalMs > 0 ? now + q.refreshIntervalMs : Infinity
|
|
1165
|
+
nextRefreshAt: q.refreshIntervalMs > 0 ? now + q.refreshIntervalMs : Infinity,
|
|
1166
|
+
...deps2
|
|
889
1167
|
});
|
|
890
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
|
+
}
|
|
891
1191
|
};
|
|
892
1192
|
var createHotCache = (options) => new HotCache(options);
|
|
893
1193
|
|
|
@@ -898,6 +1198,12 @@ var cacheCollectionKey = (dbName, physical) => `${dbName}${NS_SEP}${physical}`;
|
|
|
898
1198
|
var QueryCache = class {
|
|
899
1199
|
lru;
|
|
900
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();
|
|
901
1207
|
/** Per-collection version — bumped on every invalidation/clear of that collection. */
|
|
902
1208
|
colVersions = /* @__PURE__ */ new Map();
|
|
903
1209
|
ttlMs;
|
|
@@ -909,7 +1215,10 @@ var QueryCache = class {
|
|
|
909
1215
|
invalidateEvents = 0;
|
|
910
1216
|
clearEvents = 0;
|
|
911
1217
|
constructor(options = {}) {
|
|
912
|
-
this.lru = new LRU({
|
|
1218
|
+
this.lru = new LRU({
|
|
1219
|
+
max: options.maxSize ?? 500,
|
|
1220
|
+
onEvict: (key) => this.unregisterKey(key)
|
|
1221
|
+
});
|
|
913
1222
|
this.ttlMs = options.ttlMs ?? 0;
|
|
914
1223
|
this.clone = options.clone ?? false;
|
|
915
1224
|
}
|
|
@@ -949,14 +1258,14 @@ var QueryCache = class {
|
|
|
949
1258
|
return void 0;
|
|
950
1259
|
}
|
|
951
1260
|
if (this.ttlMs > 0 && Date.now() > entry.expiresAt) {
|
|
952
|
-
this.
|
|
1261
|
+
this.dropKey(key);
|
|
953
1262
|
this.misses++;
|
|
954
1263
|
return void 0;
|
|
955
1264
|
}
|
|
956
1265
|
if (entry.versions) {
|
|
957
1266
|
for (const [col, ver] of Object.entries(entry.versions)) {
|
|
958
1267
|
if (this.versionOf(col) !== ver) {
|
|
959
|
-
this.
|
|
1268
|
+
this.dropKey(key);
|
|
960
1269
|
this.misses++;
|
|
961
1270
|
return void 0;
|
|
962
1271
|
}
|
|
@@ -965,6 +1274,23 @@ var QueryCache = class {
|
|
|
965
1274
|
this.hits++;
|
|
966
1275
|
return this.clone ? cloneDeep(entry.value) : entry.value;
|
|
967
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
|
+
}
|
|
968
1294
|
set(key, value, ttlMs, collections, versions) {
|
|
969
1295
|
this.sets++;
|
|
970
1296
|
const ttl = ttlMs ?? this.ttlMs;
|
|
@@ -977,6 +1303,7 @@ var QueryCache = class {
|
|
|
977
1303
|
const sep = key.indexOf(SEP);
|
|
978
1304
|
const primary = sep === -1 ? key : key.slice(0, sep);
|
|
979
1305
|
const collectionKeys = /* @__PURE__ */ new Set([primary, ...collections ?? []]);
|
|
1306
|
+
this.keyCollections.set(key, collectionKeys);
|
|
980
1307
|
for (const collection of collectionKeys) {
|
|
981
1308
|
let keys = this.index.get(collection);
|
|
982
1309
|
if (!keys) {
|
|
@@ -988,7 +1315,7 @@ var QueryCache = class {
|
|
|
988
1315
|
}
|
|
989
1316
|
delete(key) {
|
|
990
1317
|
this.deletes++;
|
|
991
|
-
this.
|
|
1318
|
+
this.dropKey(key);
|
|
992
1319
|
}
|
|
993
1320
|
/** Drop every cached entry belonging to a physical collection. */
|
|
994
1321
|
invalidateByCollection(collection) {
|
|
@@ -996,7 +1323,7 @@ var QueryCache = class {
|
|
|
996
1323
|
const keys = this.index.get(collection);
|
|
997
1324
|
if (!keys) return;
|
|
998
1325
|
this.invalidateEvents++;
|
|
999
|
-
for (const key of keys) this.
|
|
1326
|
+
for (const key of [...keys]) this.dropKey(key);
|
|
1000
1327
|
this.index.delete(collection);
|
|
1001
1328
|
}
|
|
1002
1329
|
clear() {
|
|
@@ -1006,21 +1333,35 @@ var QueryCache = class {
|
|
|
1006
1333
|
}
|
|
1007
1334
|
this.lru.clear();
|
|
1008
1335
|
this.index.clear();
|
|
1336
|
+
this.keyCollections.clear();
|
|
1009
1337
|
}
|
|
1010
1338
|
};
|
|
1011
1339
|
|
|
1012
1340
|
// src/graceful-transaction.ts
|
|
1013
1341
|
var withGracefulMongoTransaction = async (runner, fn, options = {}) => {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
+
}
|
|
1017
1356
|
if (!enabled) return fn(null);
|
|
1018
1357
|
try {
|
|
1019
1358
|
return await runner.transaction(fn);
|
|
1020
1359
|
} catch (err) {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1360
|
+
if (isTransactionUnsupportedError(err)) {
|
|
1361
|
+
options.logger?.warn?.(
|
|
1362
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
1363
|
+
"transactions unsupported, falling back"
|
|
1364
|
+
);
|
|
1024
1365
|
return fn(null);
|
|
1025
1366
|
}
|
|
1026
1367
|
if (options.wrapMongoErrors !== false) {
|
|
@@ -1119,13 +1460,17 @@ var DataLoader = class {
|
|
|
1119
1460
|
}
|
|
1120
1461
|
}
|
|
1121
1462
|
async flush() {
|
|
1122
|
-
if (this.flushing)
|
|
1463
|
+
if (this.flushing) {
|
|
1464
|
+
this.scheduled = false;
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1123
1467
|
if (this.queue.length === 0) {
|
|
1124
1468
|
this.scheduled = false;
|
|
1125
1469
|
return;
|
|
1126
1470
|
}
|
|
1127
1471
|
const items = this.queue.splice(0, this.maxBatchSize);
|
|
1128
1472
|
this.flushing = true;
|
|
1473
|
+
this.scheduled = false;
|
|
1129
1474
|
try {
|
|
1130
1475
|
const resultMap = await this.batchFn(items.map((item) => item.key));
|
|
1131
1476
|
for (const item of items) {
|
|
@@ -1140,12 +1485,8 @@ var DataLoader = class {
|
|
|
1140
1485
|
}
|
|
1141
1486
|
} finally {
|
|
1142
1487
|
this.flushing = false;
|
|
1143
|
-
this.scheduled
|
|
1144
|
-
|
|
1145
|
-
this.scheduled = true;
|
|
1146
|
-
queueMicrotask(() => {
|
|
1147
|
-
void this.flush();
|
|
1148
|
-
});
|
|
1488
|
+
if (!this.scheduled && this.queue.length > 0) {
|
|
1489
|
+
this.scheduleFlush();
|
|
1149
1490
|
}
|
|
1150
1491
|
}
|
|
1151
1492
|
}
|
|
@@ -1156,6 +1497,7 @@ var canonicalKey = (value) => {
|
|
|
1156
1497
|
const anyValue = value;
|
|
1157
1498
|
if (typeof anyValue.toHexString === "function") return `oid:${anyValue.toHexString()}`;
|
|
1158
1499
|
if (value instanceof Date) return `date:${value.getTime()}`;
|
|
1500
|
+
return `obj:${stableStringify(value)}`;
|
|
1159
1501
|
}
|
|
1160
1502
|
return `${typeof value}:${String(value)}`;
|
|
1161
1503
|
};
|
|
@@ -1166,13 +1508,14 @@ var JOURNAL_COLLECTION = "_migrations";
|
|
|
1166
1508
|
|
|
1167
1509
|
// src/migrations/files.ts
|
|
1168
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);
|
|
1169
1512
|
var listMigrationFiles = async (dir) => {
|
|
1170
1513
|
const files = await readdir(dir);
|
|
1171
|
-
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));
|
|
1172
1515
|
};
|
|
1173
1516
|
var loadMigration = async (dir, file) => {
|
|
1174
1517
|
const mod = await import(pathToFileURL(join(dir, file)).href);
|
|
1175
|
-
return { name: migrationNameOf(file)
|
|
1518
|
+
return { ...mod, name: migrationNameOf(file) };
|
|
1176
1519
|
};
|
|
1177
1520
|
var ensureMigrationDir = async (dir) => {
|
|
1178
1521
|
await mkdir(dir, { recursive: true });
|
|
@@ -1186,45 +1529,78 @@ var nextMigrationNumber = async (dir) => {
|
|
|
1186
1529
|
};
|
|
1187
1530
|
|
|
1188
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
|
+
};
|
|
1189
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
|
+
};
|
|
1190
1547
|
const appliedNames = async () => {
|
|
1191
1548
|
const rows = await manager.findMany(JOURNAL_COLLECTION);
|
|
1192
1549
|
return new Set(
|
|
1193
1550
|
rows.filter((row) => row.status === void 0 || row.status === "applied").map((row) => row.name)
|
|
1194
1551
|
);
|
|
1195
1552
|
};
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
+
}
|
|
1202
1586
|
};
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1587
|
+
const renew = async (name, leaseMs) => {
|
|
1588
|
+
await manager.updateOne(
|
|
1205
1589
|
JOURNAL_COLLECTION,
|
|
1206
|
-
{ name },
|
|
1207
|
-
{
|
|
1208
|
-
$setOnInsert: {
|
|
1209
|
-
name,
|
|
1210
|
-
appliedAt: /* @__PURE__ */ new Date(),
|
|
1211
|
-
status: "running",
|
|
1212
|
-
file
|
|
1213
|
-
}
|
|
1214
|
-
},
|
|
1215
|
-
{ upsert: true, returnDocument: "before" }
|
|
1590
|
+
{ name, status: "running" },
|
|
1591
|
+
{ $set: { expiresAt: new Date(Date.now() + leaseMs) } }
|
|
1216
1592
|
);
|
|
1217
|
-
return claimed === null;
|
|
1218
1593
|
};
|
|
1219
1594
|
const markApplied = async (name) => {
|
|
1220
|
-
await manager.updateOne(
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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(() => {
|
|
1226
1602
|
});
|
|
1227
|
-
}
|
|
1603
|
+
}
|
|
1228
1604
|
};
|
|
1229
1605
|
const removeRunning = async (name) => {
|
|
1230
1606
|
await manager.deleteOne(JOURNAL_COLLECTION, { name, status: "running" }).catch(() => {
|
|
@@ -1233,45 +1609,76 @@ var createMigrationJournal = (manager) => {
|
|
|
1233
1609
|
const unrecord = async (name) => {
|
|
1234
1610
|
await manager.deleteOne(JOURNAL_COLLECTION, { name });
|
|
1235
1611
|
};
|
|
1236
|
-
return { appliedNames,
|
|
1612
|
+
return { appliedNames, claim, renew, markApplied, removeRunning, unrecord };
|
|
1237
1613
|
};
|
|
1238
1614
|
|
|
1239
1615
|
// src/migrations/index.ts
|
|
1616
|
+
var DEFAULT_LEASE_MS = 12e4;
|
|
1240
1617
|
var createMongoMigrationRunner = (service, options = {}) => {
|
|
1241
1618
|
const migrationDir = options.migrationDir ?? service.config?.migrationDir ?? "./migrations";
|
|
1242
1619
|
const logger = options.logger ?? service.logger;
|
|
1620
|
+
const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
1243
1621
|
const manager = () => {
|
|
1244
|
-
const
|
|
1622
|
+
const keys = Object.keys(service.db);
|
|
1623
|
+
const key = options.db !== void 0 ? options.db : keys[0];
|
|
1245
1624
|
const db = key ? service.db[key] : void 0;
|
|
1246
|
-
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
|
+
}
|
|
1247
1630
|
return db;
|
|
1248
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
|
+
};
|
|
1249
1643
|
const up = async () => {
|
|
1250
1644
|
await ensureMigrationDir(migrationDir);
|
|
1251
1645
|
const journal = createMigrationJournal(manager());
|
|
1252
|
-
await journal.cleanupStale();
|
|
1253
1646
|
const files = await listMigrationFiles(migrationDir);
|
|
1254
1647
|
const applied = await journal.appliedNames();
|
|
1255
1648
|
const pending = files.filter((file) => !applied.has(migrationNameOf(file)));
|
|
1256
1649
|
for (const file of pending) {
|
|
1257
1650
|
const migration = await loadMigration(migrationDir, file);
|
|
1258
1651
|
logger.info({ name: migration.name }, "running migration up");
|
|
1259
|
-
if (!await journal.claim(migration.name, basename(file))) {
|
|
1260
|
-
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
|
+
);
|
|
1261
1657
|
continue;
|
|
1262
1658
|
}
|
|
1659
|
+
const stopHeartbeat = startHeartbeat(journal, migration.name);
|
|
1263
1660
|
try {
|
|
1264
1661
|
await migration.up({ service, logger, name: migration.name });
|
|
1265
1662
|
await journal.markApplied(migration.name);
|
|
1266
1663
|
} catch (err) {
|
|
1267
1664
|
await journal.removeRunning(migration.name);
|
|
1268
1665
|
throw err;
|
|
1666
|
+
} finally {
|
|
1667
|
+
stopHeartbeat();
|
|
1269
1668
|
}
|
|
1270
1669
|
logger.info({ name: migration.name }, "migration applied");
|
|
1271
1670
|
}
|
|
1272
1671
|
};
|
|
1273
1672
|
const down = async (targetName) => {
|
|
1274
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
|
+
}
|
|
1275
1682
|
const journal = createMigrationJournal(manager());
|
|
1276
1683
|
const applied = await journal.appliedNames();
|
|
1277
1684
|
const appliedFiles = files.filter((file) => applied.has(migrationNameOf(file))).reverse();
|
|
@@ -1281,7 +1688,7 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1281
1688
|
await migration.down({ service, logger, name: migration.name });
|
|
1282
1689
|
await journal.unrecord(migration.name);
|
|
1283
1690
|
logger.info({ name: migration.name }, "migration rolled back");
|
|
1284
|
-
if (targetName && migration.name === targetName) break;
|
|
1691
|
+
if (targetName !== void 0 && migration.name === targetName) break;
|
|
1285
1692
|
}
|
|
1286
1693
|
};
|
|
1287
1694
|
const status = async () => {
|
|
@@ -1295,10 +1702,6 @@ var createMongoMigrationRunner = (service, options = {}) => {
|
|
|
1295
1702
|
};
|
|
1296
1703
|
const create = async (name) => {
|
|
1297
1704
|
await ensureMigrationDir(migrationDir);
|
|
1298
|
-
const next = await nextMigrationNumber(migrationDir);
|
|
1299
|
-
const padded = String(next).padStart(3, "0");
|
|
1300
|
-
const fileName = `${padded}_${name}.ts`;
|
|
1301
|
-
const filePath = join(migrationDir, fileName);
|
|
1302
1705
|
const template = `import type { MigrationContext } from '@ignex/ninox';
|
|
1303
1706
|
|
|
1304
1707
|
export const up = async (ctx: MigrationContext): Promise<void> => {
|
|
@@ -1311,15 +1714,26 @@ export const down = async (ctx: MigrationContext): Promise<void> => {
|
|
|
1311
1714
|
// await users.client.dropCollection('users');
|
|
1312
1715
|
};
|
|
1313
1716
|
`;
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
+
}
|
|
1317
1731
|
};
|
|
1318
1732
|
return { up, down, status, create };
|
|
1319
1733
|
};
|
|
1320
1734
|
|
|
1321
1735
|
// src/mongo-helpers.ts
|
|
1322
|
-
var withRetry = async (fn, options = {}
|
|
1736
|
+
var withRetry = async (fn, options = {}) => {
|
|
1323
1737
|
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
1324
1738
|
const delayMs = options.delayMs ?? 150;
|
|
1325
1739
|
const logger = options.logger;
|
|
@@ -1332,7 +1746,8 @@ var withRetry = async (fn, options = {}, _ctx) => {
|
|
|
1332
1746
|
lastError = err;
|
|
1333
1747
|
attempt += 1;
|
|
1334
1748
|
if (attempt >= maxAttempts || !isMongoTransientError(err)) throw err;
|
|
1335
|
-
const
|
|
1749
|
+
const base = delayMs * 2 ** (attempt - 1);
|
|
1750
|
+
const backoff = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
1336
1751
|
logger?.debug?.({ attempt, backoffMs: backoff, error: err?.message }, "retrying");
|
|
1337
1752
|
await sleep(backoff);
|
|
1338
1753
|
}
|
|
@@ -1362,7 +1777,13 @@ var DEFAULT_FIND_LIMIT = 100;
|
|
|
1362
1777
|
var MAX_BATCH_OPS = 1e5;
|
|
1363
1778
|
|
|
1364
1779
|
// src/query-builder/query-builder.ts
|
|
1365
|
-
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 {
|
|
1366
1787
|
constructor(ctx) {
|
|
1367
1788
|
this.ctx = ctx;
|
|
1368
1789
|
}
|
|
@@ -1375,77 +1796,112 @@ var QueryBuilder = class {
|
|
|
1375
1796
|
get filter() {
|
|
1376
1797
|
return this._filter;
|
|
1377
1798
|
}
|
|
1378
|
-
/**
|
|
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). */
|
|
1379
1811
|
where(filter) {
|
|
1380
|
-
|
|
1381
|
-
|
|
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;
|
|
1382
1819
|
}
|
|
1383
|
-
/** AND-combine the current filter with a new one. */
|
|
1820
|
+
/** AND-combine the current filter with a new one (stays flat across repeats). */
|
|
1384
1821
|
and(filter) {
|
|
1385
|
-
|
|
1386
|
-
|
|
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;
|
|
1387
1827
|
}
|
|
1388
|
-
/**
|
|
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
|
+
*/
|
|
1389
1832
|
or(...filters) {
|
|
1390
|
-
|
|
1391
|
-
|
|
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 });
|
|
1392
1837
|
}
|
|
1393
1838
|
/** Set the sort order (`{ field: 1 | -1 }`). */
|
|
1394
1839
|
sort(sort) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1840
|
+
const next = this.fork();
|
|
1841
|
+
next._sort = sort;
|
|
1842
|
+
return next;
|
|
1397
1843
|
}
|
|
1398
1844
|
/** Skip the first `n` matching documents. */
|
|
1399
1845
|
skip(n) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1846
|
+
const next = this.fork();
|
|
1847
|
+
next._skip = n;
|
|
1848
|
+
return next;
|
|
1402
1849
|
}
|
|
1403
1850
|
/** Limit the result set to `n` documents. */
|
|
1404
1851
|
limit(n) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1852
|
+
const next = this.fork();
|
|
1853
|
+
next._limit = n;
|
|
1854
|
+
return next;
|
|
1407
1855
|
}
|
|
1408
1856
|
/** Project only the given fields — the projection is pushed to the driver. */
|
|
1409
1857
|
select(fields) {
|
|
1858
|
+
const next = this.fork();
|
|
1410
1859
|
const projection = {};
|
|
1411
1860
|
for (const field of fields) projection[field] = 1;
|
|
1412
|
-
|
|
1413
|
-
return
|
|
1861
|
+
next._projection = projection;
|
|
1862
|
+
return next;
|
|
1414
1863
|
}
|
|
1415
1864
|
/** Raw projection document (`{ field: 1 }` / `{ field: 0 }`). */
|
|
1416
1865
|
project(projection) {
|
|
1417
|
-
|
|
1418
|
-
|
|
1866
|
+
const next = this.fork();
|
|
1867
|
+
next._projection = projection;
|
|
1868
|
+
return next;
|
|
1419
1869
|
}
|
|
1420
1870
|
/** Set the index hint. */
|
|
1421
1871
|
hint(hint) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1872
|
+
const next = this.fork();
|
|
1873
|
+
next._options.hint = hint;
|
|
1874
|
+
return next;
|
|
1424
1875
|
}
|
|
1425
1876
|
/** Set the driver cursor batch size. */
|
|
1426
1877
|
batchSize(n) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1878
|
+
const next = this.fork();
|
|
1879
|
+
next._options.batchSize = n;
|
|
1880
|
+
return next;
|
|
1429
1881
|
}
|
|
1430
1882
|
/** Run the query inside a transaction session. */
|
|
1431
1883
|
session(session) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1884
|
+
const next = this.fork();
|
|
1885
|
+
next._options.session = session;
|
|
1886
|
+
return next;
|
|
1434
1887
|
}
|
|
1435
1888
|
/** Cap server-side execution time. */
|
|
1436
1889
|
maxTimeMS(ms) {
|
|
1437
|
-
|
|
1438
|
-
|
|
1890
|
+
const next = this.fork();
|
|
1891
|
+
next._options.maxTimeMS = ms;
|
|
1892
|
+
return next;
|
|
1439
1893
|
}
|
|
1440
1894
|
/** Bypass the read cache for this query when `false`. */
|
|
1441
1895
|
cache(on) {
|
|
1442
|
-
|
|
1443
|
-
|
|
1896
|
+
const next = this.fork();
|
|
1897
|
+
next._options.cache = on;
|
|
1898
|
+
return next;
|
|
1444
1899
|
}
|
|
1445
1900
|
/** Override in-flight dedup for this query when set. */
|
|
1446
1901
|
dedupe(on) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1902
|
+
const next = this.fork();
|
|
1903
|
+
next._options.dedupe = on;
|
|
1904
|
+
return next;
|
|
1449
1905
|
}
|
|
1450
1906
|
/** Compile the builder into driver options (does not execute). */
|
|
1451
1907
|
compile() {
|
|
@@ -1487,7 +1943,7 @@ var QueryBuilder = class {
|
|
|
1487
1943
|
options
|
|
1488
1944
|
);
|
|
1489
1945
|
}
|
|
1490
|
-
/** Streaming cursor (raw driver cursor; no retry
|
|
1946
|
+
/** Streaming cursor (raw driver cursor; no retry/cache/drift — caller-owned). */
|
|
1491
1947
|
cursor() {
|
|
1492
1948
|
return this.ctx.collection.find(
|
|
1493
1949
|
this._filter,
|
|
@@ -1503,9 +1959,21 @@ var QueryBuilder = class {
|
|
|
1503
1959
|
this.countOptions()
|
|
1504
1960
|
);
|
|
1505
1961
|
}
|
|
1506
|
-
/** True when at least one document matches. */
|
|
1962
|
+
/** True when at least one document matches (`_id`-only projection — no doc transfer). */
|
|
1507
1963
|
async exists() {
|
|
1508
|
-
|
|
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;
|
|
1509
1977
|
}
|
|
1510
1978
|
};
|
|
1511
1979
|
|
|
@@ -1536,8 +2004,9 @@ var createRepository = (manager, collection, options = {}) => {
|
|
|
1536
2004
|
findOne: (filter, o) => manager.getOne(collection, filter, o),
|
|
1537
2005
|
findOneOrFail: (filter, o) => manager.getOneOrFail(collection, filter, o),
|
|
1538
2006
|
findMany: (filter, o) => manager.findMany(collection, filter, {
|
|
1539
|
-
|
|
1540
|
-
...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 ?? {}
|
|
1541
2010
|
}),
|
|
1542
2011
|
findActive: (filter, o) => manager.findActive(collection, filter, o),
|
|
1543
2012
|
count: (filter, o) => manager.countDocuments(collection, filter, o),
|
|
@@ -1565,9 +2034,25 @@ var createRepository = (manager, collection, options = {}) => {
|
|
|
1565
2034
|
|
|
1566
2035
|
// src/schema/collections.ts
|
|
1567
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
|
+
};
|
|
1568
2050
|
var resolveItemValue = (item) => {
|
|
1569
2051
|
if ("schema" in item) {
|
|
1570
2052
|
const def = item;
|
|
2053
|
+
if (def.timestamps !== void 0) {
|
|
2054
|
+
assertTimestampsDeclared(def.name, def.schema, def.timestamps);
|
|
2055
|
+
}
|
|
1571
2056
|
return {
|
|
1572
2057
|
schema: def.schema,
|
|
1573
2058
|
...def.indexes ? { indexes: def.indexes } : {},
|
|
@@ -1579,6 +2064,9 @@ var resolveItemValue = (item) => {
|
|
|
1579
2064
|
};
|
|
1580
2065
|
var defineCollection = (name, schema, extras = {}) => {
|
|
1581
2066
|
const namedSchema = { ...schema, name };
|
|
2067
|
+
if (extras.timestamps !== void 0) {
|
|
2068
|
+
assertTimestampsDeclared(name, schema, extras.timestamps);
|
|
2069
|
+
}
|
|
1582
2070
|
return {
|
|
1583
2071
|
name,
|
|
1584
2072
|
schema: namedSchema,
|
|
@@ -1608,6 +2096,11 @@ var addNumericBounds = (out, field) => {
|
|
|
1608
2096
|
if (field.exclusiveMaximum) out.exclusiveMaximum = true;
|
|
1609
2097
|
return out;
|
|
1610
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"];
|
|
1611
2104
|
var convertField = (field) => {
|
|
1612
2105
|
if (field.kind === "raw") {
|
|
1613
2106
|
const out2 = { ...field.fragment };
|
|
@@ -1624,13 +2117,15 @@ var convertField = (field) => {
|
|
|
1624
2117
|
if (field.pattern) out.pattern = field.pattern;
|
|
1625
2118
|
return out;
|
|
1626
2119
|
case "number":
|
|
1627
|
-
out.bsonType = field.integer ?
|
|
2120
|
+
out.bsonType = field.integer ? [...INTEGER_WIRE_UNION] : "number";
|
|
2121
|
+
if (field.integer && field.multipleOf === void 0) out.multipleOf = 1;
|
|
1628
2122
|
return addNumericBounds(out, field);
|
|
1629
2123
|
case "double":
|
|
1630
|
-
out.bsonType =
|
|
2124
|
+
out.bsonType = [...NUMERIC_WIRE_UNION.double];
|
|
1631
2125
|
return addNumericBounds(out, field);
|
|
1632
2126
|
case "long":
|
|
1633
|
-
out.bsonType =
|
|
2127
|
+
out.bsonType = [...NUMERIC_WIRE_UNION.long];
|
|
2128
|
+
if (field.multipleOf === void 0) out.multipleOf = 1;
|
|
1634
2129
|
return addNumericBounds(out, field);
|
|
1635
2130
|
case "decimal":
|
|
1636
2131
|
out.bsonType = "decimal";
|
|
@@ -1646,6 +2141,7 @@ var convertField = (field) => {
|
|
|
1646
2141
|
return out;
|
|
1647
2142
|
case "geoPoint":
|
|
1648
2143
|
return {
|
|
2144
|
+
...field.description ? { description: field.description } : {},
|
|
1649
2145
|
bsonType: "object",
|
|
1650
2146
|
required: ["type", "coordinates"],
|
|
1651
2147
|
properties: {
|
|
@@ -1671,7 +2167,10 @@ var convertField = (field) => {
|
|
|
1671
2167
|
else if (typeof v === "string") types.add("string");
|
|
1672
2168
|
else if (typeof v === "number") {
|
|
1673
2169
|
const allInt = field.values.every((x) => typeof x !== "number" || Number.isInteger(x));
|
|
1674
|
-
|
|
2170
|
+
const fitsInt32 = allInt && field.values.every(
|
|
2171
|
+
(x) => typeof x !== "number" || x >= -2147483648 && x <= 2147483647
|
|
2172
|
+
);
|
|
2173
|
+
types.add(allInt && fitsInt32 ? "int" : "number");
|
|
1675
2174
|
break;
|
|
1676
2175
|
}
|
|
1677
2176
|
}
|
|
@@ -1825,6 +2324,10 @@ var numericValue = (value) => {
|
|
|
1825
2324
|
const n = Number(value.toString());
|
|
1826
2325
|
return Number.isNaN(n) ? void 0 : n;
|
|
1827
2326
|
}
|
|
2327
|
+
if (value instanceof Long) {
|
|
2328
|
+
const n = value.toNumber();
|
|
2329
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
2330
|
+
}
|
|
1828
2331
|
if (typeof value === "number" && !Number.isNaN(value)) return value;
|
|
1829
2332
|
return void 0;
|
|
1830
2333
|
};
|
|
@@ -1882,7 +2385,7 @@ var bsonTypeMatches = (bsonType, value) => {
|
|
|
1882
2385
|
case "double":
|
|
1883
2386
|
return typeof value === "number" && !Number.isNaN(value);
|
|
1884
2387
|
case "long":
|
|
1885
|
-
return typeof value === "number" && Number.isInteger(value);
|
|
2388
|
+
return typeof value === "number" && Number.isInteger(value) || value instanceof Long;
|
|
1886
2389
|
case "decimal":
|
|
1887
2390
|
return value instanceof Decimal128;
|
|
1888
2391
|
default:
|
|
@@ -1904,6 +2407,19 @@ var checkReservedValue = (key, value, path, issues) => {
|
|
|
1904
2407
|
};
|
|
1905
2408
|
|
|
1906
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
|
+
};
|
|
1907
2423
|
var stringValidator = (_deps, field, value, path, issues) => {
|
|
1908
2424
|
const f = field;
|
|
1909
2425
|
if (typeof value !== "string") {
|
|
@@ -1928,19 +2444,15 @@ var stringValidator = (_deps, field, value, path, issues) => {
|
|
|
1928
2444
|
`Length ${value.length} > ${f.maxLength}`
|
|
1929
2445
|
);
|
|
1930
2446
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
);
|
|
1941
|
-
}
|
|
1942
|
-
} catch {
|
|
1943
|
-
}
|
|
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
|
+
);
|
|
1944
2456
|
}
|
|
1945
2457
|
};
|
|
1946
2458
|
var numberValidator = (_deps, field, value, path, issues) => {
|
|
@@ -1957,7 +2469,7 @@ var numberValidator = (_deps, field, value, path, issues) => {
|
|
|
1957
2469
|
};
|
|
1958
2470
|
var floatingValidator = (_deps, field, value, path, issues) => {
|
|
1959
2471
|
const f = field;
|
|
1960
|
-
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);
|
|
1961
2473
|
if (!ok) {
|
|
1962
2474
|
push(issues, path, "type", f.kind, `Expected ${f.kind}, got ${describeValue(value)}`);
|
|
1963
2475
|
return;
|
|
@@ -2114,7 +2626,7 @@ function validateObjectProps(field, value, path, issues, topLevel) {
|
|
|
2114
2626
|
validateValue(prop, value[key], propPath(key), issues);
|
|
2115
2627
|
}
|
|
2116
2628
|
if (field.additionalProperties !== true) {
|
|
2117
|
-
const allowedReserved = topLevel ?
|
|
2629
|
+
const allowedReserved = topLevel ? ALLOWED_RESERVED_TOP : NO_RESERVED;
|
|
2118
2630
|
for (const key of Object.keys(value)) {
|
|
2119
2631
|
if (key in field.properties) continue;
|
|
2120
2632
|
if (allowedReserved.has(key)) continue;
|
|
@@ -2161,6 +2673,11 @@ function validateObjectProps(field, value, path, issues, topLevel) {
|
|
|
2161
2673
|
}
|
|
2162
2674
|
}
|
|
2163
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();
|
|
2164
2681
|
var validateDoc = (schema, doc) => {
|
|
2165
2682
|
const issues = [];
|
|
2166
2683
|
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
@@ -2187,15 +2704,23 @@ var createResolveCollectionName = (config) => {
|
|
|
2187
2704
|
// src/service/connection-uri.ts
|
|
2188
2705
|
var normalizeMongoUrl = (rawUrl) => {
|
|
2189
2706
|
try {
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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");
|
|
2197
2721
|
}
|
|
2198
|
-
|
|
2722
|
+
const queryString = params.toString();
|
|
2723
|
+
return queryString ? `${base}?${queryString}` : base;
|
|
2199
2724
|
} catch {
|
|
2200
2725
|
return rawUrl;
|
|
2201
2726
|
}
|
|
@@ -2231,7 +2756,12 @@ var CacheInvalidator = class {
|
|
|
2231
2756
|
const key = `${ref.db.databaseName}${SEP2}${ref.collection}`;
|
|
2232
2757
|
if (seen.has(key) || this.streams.has(key)) continue;
|
|
2233
2758
|
seen.add(key);
|
|
2234
|
-
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
|
+
});
|
|
2235
2765
|
}
|
|
2236
2766
|
}
|
|
2237
2767
|
/** Close every watcher. Safe to call repeatedly, and before any `start`. */
|
|
@@ -2251,11 +2781,17 @@ var CacheInvalidator = class {
|
|
|
2251
2781
|
async watchLoop(key, ref) {
|
|
2252
2782
|
let backoff = 0;
|
|
2253
2783
|
while (!this.stopped) {
|
|
2784
|
+
let connectedAt = Date.now();
|
|
2254
2785
|
try {
|
|
2255
2786
|
const stream = ref.db.collection(ref.collection).watch([]);
|
|
2256
|
-
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
|
+
);
|
|
2257
2792
|
});
|
|
2258
2793
|
this.streams.set(key, stream);
|
|
2794
|
+
connectedAt = Date.now();
|
|
2259
2795
|
if (backoff > 0) {
|
|
2260
2796
|
this.cache.invalidateByCollection(
|
|
2261
2797
|
cacheCollectionKey(ref.db.databaseName, ref.collection)
|
|
@@ -2268,6 +2804,7 @@ var CacheInvalidator = class {
|
|
|
2268
2804
|
);
|
|
2269
2805
|
}
|
|
2270
2806
|
} catch (err) {
|
|
2807
|
+
if (Date.now() - connectedAt > 6e4) backoff = 0;
|
|
2271
2808
|
const message = err instanceof Error ? err.message : String(err);
|
|
2272
2809
|
const abandoned = this.streams.get(key);
|
|
2273
2810
|
this.streams.delete(key);
|
|
@@ -2373,6 +2910,7 @@ var makeGetDbUrl = (dbClients, config) => (name) => {
|
|
|
2373
2910
|
return normalizeMongoUrl(config.defaultDb);
|
|
2374
2911
|
};
|
|
2375
2912
|
var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClients = /* @__PURE__ */ new Map()) => {
|
|
2913
|
+
const connecting = /* @__PURE__ */ new Map();
|
|
2376
2914
|
return async (names) => {
|
|
2377
2915
|
const keys = names && names.length > 0 ? names : Object.keys(dbClients);
|
|
2378
2916
|
const openedThisCall = [];
|
|
@@ -2383,18 +2921,32 @@ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClien
|
|
|
2383
2921
|
const dbUrl = getDbUrl(key);
|
|
2384
2922
|
let client = openedClients.get(dbUrl);
|
|
2385
2923
|
if (!client) {
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
+
}
|
|
2398
2950
|
}
|
|
2399
2951
|
const dbKey = `${key}Client`;
|
|
2400
2952
|
db[dbKey] = buildManager2(key, definition.name, client);
|
|
@@ -2422,23 +2974,32 @@ var closeConnections = async (openedClients) => {
|
|
|
2422
2974
|
};
|
|
2423
2975
|
|
|
2424
2976
|
// src/service/health.ts
|
|
2425
|
-
var createHealth = (db) => async () => {
|
|
2977
|
+
var createHealth = (db, options = {}) => async () => {
|
|
2426
2978
|
const started = performance.now();
|
|
2979
|
+
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
2427
2980
|
const dbs = await Promise.all(
|
|
2428
2981
|
Object.entries(db).map(async ([key, manager]) => {
|
|
2429
2982
|
const name = key.replace(/Client$/, "");
|
|
2430
2983
|
const handle = manager?.client;
|
|
2431
|
-
if (!handle) return { name, ok: false };
|
|
2984
|
+
if (!handle) return { name, ok: false, error: "no client handle on manager" };
|
|
2432
2985
|
try {
|
|
2433
2986
|
const t0 = performance.now();
|
|
2434
|
-
await withTimeout(handle.admin().command({ ping: 1 }),
|
|
2987
|
+
await withTimeout(handle.admin().command({ ping: 1 }), timeoutMs, `health:${name}`);
|
|
2435
2988
|
return { name, ok: true, latencyMs: performance.now() - t0 };
|
|
2436
|
-
} catch {
|
|
2437
|
-
return {
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
return {
|
|
2991
|
+
name,
|
|
2992
|
+
ok: false,
|
|
2993
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2994
|
+
};
|
|
2438
2995
|
}
|
|
2439
2996
|
})
|
|
2440
2997
|
);
|
|
2441
|
-
return {
|
|
2998
|
+
return {
|
|
2999
|
+
ok: dbs.length > 0 && dbs.every((r) => r.ok),
|
|
3000
|
+
latencyMs: performance.now() - started,
|
|
3001
|
+
dbs
|
|
3002
|
+
};
|
|
2442
3003
|
};
|
|
2443
3004
|
|
|
2444
3005
|
// src/relation/populate.ts
|
|
@@ -2484,57 +3045,72 @@ var distinctKeys = (values) => {
|
|
|
2484
3045
|
};
|
|
2485
3046
|
var makePopulator = (deps2) => {
|
|
2486
3047
|
const populate = async (docs, relations, options = {}) => {
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
const joins = key === void 0 || key === null ? [] : throughByKey.get(canonicalKey(key)) ?? [];
|
|
2530
|
-
const targets = joins.flatMap(
|
|
2531
|
-
(join3) => targetByKey.get(canonicalKey(join3[relation.through.foreignField])) ?? []
|
|
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
|
|
2532
3090
|
);
|
|
2533
|
-
|
|
3091
|
+
const joinDocs = [...throughByKey.values()].flat();
|
|
3092
|
+
const targetKeys = distinctKeys(
|
|
3093
|
+
joinDocs.map((join3) => join3[relation.through.foreignField])
|
|
3094
|
+
);
|
|
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
|
+
}
|
|
2534
3110
|
}
|
|
2535
|
-
}
|
|
2536
|
-
|
|
2537
|
-
return
|
|
3111
|
+
})
|
|
3112
|
+
);
|
|
3113
|
+
return out;
|
|
2538
3114
|
};
|
|
2539
3115
|
return { populate };
|
|
2540
3116
|
};
|
|
@@ -2566,13 +3142,28 @@ var probeDebugQuery = async () => {
|
|
|
2566
3142
|
debugQueryCache = null;
|
|
2567
3143
|
}
|
|
2568
3144
|
};
|
|
3145
|
+
var sanitizeDebugParams = (value) => {
|
|
3146
|
+
try {
|
|
3147
|
+
return JSON.parse(JSON.stringify(value));
|
|
3148
|
+
} catch {
|
|
3149
|
+
try {
|
|
3150
|
+
return String(value);
|
|
3151
|
+
} catch {
|
|
3152
|
+
return "[unserializable]";
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
};
|
|
2569
3156
|
var traceDbOp = async (logger, meta, fn, options = {}) => {
|
|
2570
3157
|
const start = performance.now();
|
|
2571
3158
|
logger.debug({ ...meta, operation: "start" }, "start");
|
|
2572
3159
|
try {
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3160
|
+
let debugQuery = options.debugQuery;
|
|
3161
|
+
if (debugQuery === void 0) {
|
|
3162
|
+
if (debugQueryCache === void 0) await probeDebugQuery();
|
|
3163
|
+
debugQuery = debugQueryCache;
|
|
3164
|
+
}
|
|
3165
|
+
const params = meta.params === void 0 ? void 0 : sanitizeDebugParams(meta.params);
|
|
3166
|
+
const result = debugQuery ? await debugQuery(`${meta.collection}.${meta.op}`, params, () => fn()) : await fn();
|
|
2576
3167
|
const durationMs = performance.now() - start;
|
|
2577
3168
|
logger.info({ ...meta, durationMs }, "ok");
|
|
2578
3169
|
return result;
|
|
@@ -2588,11 +3179,12 @@ var traceDbOp = async (logger, meta, fn, options = {}) => {
|
|
|
2588
3179
|
// src/service/op-deps.ts
|
|
2589
3180
|
var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors) => {
|
|
2590
3181
|
const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, { wrapMongoErrors });
|
|
2591
|
-
const meta = (collection, op) => ({
|
|
3182
|
+
const meta = (collection, op, params) => ({
|
|
2592
3183
|
collection: String(collection),
|
|
2593
3184
|
physicalCollection: resolve(String(collection)),
|
|
2594
3185
|
db: dbLabel,
|
|
2595
|
-
op
|
|
3186
|
+
op,
|
|
3187
|
+
...params !== void 0 ? { params } : {}
|
|
2596
3188
|
});
|
|
2597
3189
|
return { trace, meta };
|
|
2598
3190
|
};
|
|
@@ -2638,11 +3230,11 @@ var resolveDriftMode = (perOp, serviceDefault) => {
|
|
|
2638
3230
|
};
|
|
2639
3231
|
|
|
2640
3232
|
// src/service/crud-op.ts
|
|
2641
|
-
var defineCrudOp = async (deps2, collection, opName, execute, options, write = false) => {
|
|
3233
|
+
var defineCrudOp = async (deps2, collection, opName, execute, options, write = false, params) => {
|
|
2642
3234
|
const resolved = resolveQueryOptions(options);
|
|
2643
3235
|
const maxAttempts = write && resolved.retryWrites !== true ? 1 : resolved.maxAttempts;
|
|
2644
3236
|
return deps2.trace(
|
|
2645
|
-
deps2.meta(collection, opName),
|
|
3237
|
+
deps2.meta(collection, opName, params),
|
|
2646
3238
|
async () => withRetry(async () => execute(resolved), {
|
|
2647
3239
|
maxAttempts,
|
|
2648
3240
|
delayMs: resolved.retryDelayMs
|
|
@@ -2747,7 +3339,9 @@ var PipelineBuilder = class _PipelineBuilder {
|
|
|
2747
3339
|
this.deps.logical,
|
|
2748
3340
|
"mongo.pipeline",
|
|
2749
3341
|
run,
|
|
2750
|
-
this.deps.options
|
|
3342
|
+
this.deps.options,
|
|
3343
|
+
false,
|
|
3344
|
+
{ pipeline: this.stages }
|
|
2751
3345
|
);
|
|
2752
3346
|
}
|
|
2753
3347
|
/** Run the pipeline and return the first output document (or null). */
|
|
@@ -2774,7 +3368,9 @@ var PipelineBuilder = class _PipelineBuilder {
|
|
|
2774
3368
|
this.deps.logical,
|
|
2775
3369
|
"mongo.pipeline.first",
|
|
2776
3370
|
run,
|
|
2777
|
-
this.deps.options
|
|
3371
|
+
this.deps.options,
|
|
3372
|
+
false,
|
|
3373
|
+
{ pipeline: this.stages }
|
|
2778
3374
|
);
|
|
2779
3375
|
}
|
|
2780
3376
|
/** Streaming cursor (raw driver cursor; no retry / trace). */
|
|
@@ -2894,7 +3490,9 @@ var makeAggregateOps = (ctx) => {
|
|
|
2894
3490
|
collection,
|
|
2895
3491
|
"mongo.aggregate",
|
|
2896
3492
|
(r) => coll(collection).aggregate(pipeline2, mergeAggOptions2(r.driverOpts, r.sdk)),
|
|
2897
|
-
options
|
|
3493
|
+
options,
|
|
3494
|
+
false,
|
|
3495
|
+
{ pipeline: pipeline2 }
|
|
2898
3496
|
);
|
|
2899
3497
|
};
|
|
2900
3498
|
const pipeline = (collection, options) => new PipelineBuilder({
|
|
@@ -2980,7 +3578,7 @@ var createCachedAggregate = (ctx) => {
|
|
|
2980
3578
|
const noSession = !resolved.sdk.session;
|
|
2981
3579
|
const useCache = cache !== void 0 && noSession && resolved.cache !== false && isCacheablePipeline(pipeline);
|
|
2982
3580
|
const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
|
|
2983
|
-
const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options);
|
|
3581
|
+
const runDriver = () => defineCrudOp(deps2, collection, opName, execute, options, false, { pipeline });
|
|
2984
3582
|
const key = useCache ? cache.key(
|
|
2985
3583
|
cacheCollectionKey(client.databaseName, physical),
|
|
2986
3584
|
stableHash([opName, pipeline, options])
|
|
@@ -3010,10 +3608,12 @@ var createCachedAggregate = (ctx) => {
|
|
|
3010
3608
|
return runDriver();
|
|
3011
3609
|
};
|
|
3012
3610
|
if (shouldDedupe) {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3611
|
+
const primaryKey = cacheCollectionKey(client.databaseName, physical);
|
|
3612
|
+
const gen = cache !== void 0 ? cache.versionOf(primaryKey) : "";
|
|
3613
|
+
const dedupeKey = `${primaryKey}|${gen}|${opName}|${stableHash([pipeline, options])}`;
|
|
3614
|
+
return await opts.inFlight.run(dedupeKey, runOnce);
|
|
3015
3615
|
}
|
|
3016
|
-
return runOnce();
|
|
3616
|
+
return await runOnce();
|
|
3017
3617
|
};
|
|
3018
3618
|
};
|
|
3019
3619
|
|
|
@@ -3235,6 +3835,54 @@ var makeAggregationOps = (client, dbLabel, logger, opts) => {
|
|
|
3235
3835
|
};
|
|
3236
3836
|
};
|
|
3237
3837
|
|
|
3838
|
+
// src/schema/apply-defaults.ts
|
|
3839
|
+
var cloneDefault = (value) => {
|
|
3840
|
+
if (value === null || typeof value !== "object") return value;
|
|
3841
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
3842
|
+
return Array.isArray(value) ? [...value] : { ...value };
|
|
3843
|
+
};
|
|
3844
|
+
var applySchemaDefaults = (schema, doc) => {
|
|
3845
|
+
if (!schema || doc === null || typeof doc !== "object" || Array.isArray(doc)) return;
|
|
3846
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3847
|
+
if (key in doc && doc[key] !== void 0) {
|
|
3848
|
+
if (prop.kind === "object" && doc[key] !== null && typeof doc[key] === "object" && !Array.isArray(doc[key])) {
|
|
3849
|
+
applySchemaDefaults(prop, doc[key]);
|
|
3850
|
+
}
|
|
3851
|
+
continue;
|
|
3852
|
+
}
|
|
3853
|
+
if (prop.flags.hasDefault) {
|
|
3854
|
+
doc[key] = cloneDefault(prop.flags.defaultValue);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
};
|
|
3858
|
+
|
|
3859
|
+
// src/utils/omit-undefined.ts
|
|
3860
|
+
var stripUndefinedKeys = (doc) => {
|
|
3861
|
+
for (const key of Object.keys(doc)) {
|
|
3862
|
+
if (doc[key] === void 0) {
|
|
3863
|
+
delete doc[key];
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return doc;
|
|
3867
|
+
};
|
|
3868
|
+
var stripUndefinedFromUpdate = (update) => {
|
|
3869
|
+
if (update === null || typeof update !== "object") return update;
|
|
3870
|
+
const doc = update;
|
|
3871
|
+
for (const operator of ["$set", "$setOnInsert"]) {
|
|
3872
|
+
const op = doc[operator];
|
|
3873
|
+
if (op !== null && typeof op === "object" && !Array.isArray(op)) {
|
|
3874
|
+
const opDoc = op;
|
|
3875
|
+
for (const key of Object.keys(opDoc)) {
|
|
3876
|
+
if (opDoc[key] === void 0) delete opDoc[key];
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
for (const key of Object.keys(doc)) {
|
|
3881
|
+
if (doc[key] === void 0) delete doc[key];
|
|
3882
|
+
}
|
|
3883
|
+
return update;
|
|
3884
|
+
};
|
|
3885
|
+
|
|
3238
3886
|
// src/service/drift.ts
|
|
3239
3887
|
var checkDocsDrift = (options, collection, op, docs) => {
|
|
3240
3888
|
if (options.drift === "off") return;
|
|
@@ -3279,8 +3927,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3279
3927
|
);
|
|
3280
3928
|
const coll = (name) => client.collection(resolve(String(name)));
|
|
3281
3929
|
const toDriverFilter = (filter) => filter;
|
|
3282
|
-
const run = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options);
|
|
3283
|
-
const writeRun = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options, true);
|
|
3930
|
+
const run = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, false, params);
|
|
3931
|
+
const writeRun = (collection, opName, execute, options, params) => defineCrudOp(deps2, collection, opName, execute, options, true, params);
|
|
3284
3932
|
const invalidate = (collection) => {
|
|
3285
3933
|
opts.cache?.invalidateByCollection(
|
|
3286
3934
|
cacheCollectionKey(client.databaseName, resolve(String(collection)))
|
|
@@ -3288,6 +3936,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3288
3936
|
};
|
|
3289
3937
|
const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
|
|
3290
3938
|
const stampCreate = (collection, doc) => {
|
|
3939
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3940
|
+
stripUndefinedKeys(doc);
|
|
3291
3941
|
const ts = timestampsFor(collection);
|
|
3292
3942
|
if (!ts) return;
|
|
3293
3943
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3295,6 +3945,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3295
3945
|
if (ts.updatedAt && doc[ts.updatedAt] === void 0) doc[ts.updatedAt] = now;
|
|
3296
3946
|
};
|
|
3297
3947
|
const stampUpdate = (collection, update) => {
|
|
3948
|
+
stripUndefinedFromUpdate(update);
|
|
3298
3949
|
const formatted = formatUpdateFilter(update);
|
|
3299
3950
|
const ts = timestampsFor(collection);
|
|
3300
3951
|
if (ts?.updatedAt) {
|
|
@@ -3304,13 +3955,18 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3304
3955
|
return formatted;
|
|
3305
3956
|
};
|
|
3306
3957
|
const stampReplace = (collection, doc) => {
|
|
3958
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3959
|
+
stripUndefinedKeys(doc);
|
|
3307
3960
|
const ts = timestampsFor(collection);
|
|
3308
3961
|
if (!ts?.updatedAt) return doc;
|
|
3309
3962
|
return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
|
|
3310
3963
|
};
|
|
3311
|
-
const read = async (collection, opName, filter, options, execute, shape = "none") => {
|
|
3964
|
+
const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
|
|
3312
3965
|
const physical = resolve(String(collection));
|
|
3313
3966
|
const resolved = resolveQueryOptions(options);
|
|
3967
|
+
const sentParams = { filter };
|
|
3968
|
+
if (Object.keys(resolved.driverOpts).length > 0) sentParams.options = resolved.driverOpts;
|
|
3969
|
+
if (keyExtra !== void 0) sentParams.field = keyExtra;
|
|
3314
3970
|
const noSession = !resolved.sdk.session;
|
|
3315
3971
|
const useCache = opts.cache !== void 0 && noSession && resolved.cache !== false;
|
|
3316
3972
|
const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
|
|
@@ -3332,16 +3988,28 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3332
3988
|
);
|
|
3333
3989
|
};
|
|
3334
3990
|
const executeWrapped = () => deps2.trace(
|
|
3335
|
-
deps2.meta(collection, opName),
|
|
3991
|
+
deps2.meta(collection, opName, sentParams),
|
|
3336
3992
|
() => withRetry(() => execute(resolved), {
|
|
3337
3993
|
maxAttempts: resolved.maxAttempts,
|
|
3338
3994
|
delayMs: resolved.retryDelayMs
|
|
3339
3995
|
})
|
|
3340
3996
|
);
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3997
|
+
const {
|
|
3998
|
+
maxAttempts: _ma,
|
|
3999
|
+
retryDelayMs: _rd,
|
|
4000
|
+
dedupe: _dd,
|
|
4001
|
+
cache: _ca,
|
|
4002
|
+
retryWrites: _rw,
|
|
4003
|
+
...resultOptions
|
|
4004
|
+
} = options ?? {};
|
|
4005
|
+
let payloadHash;
|
|
4006
|
+
const getPayloadHash = () => {
|
|
4007
|
+
if (payloadHash === void 0) {
|
|
4008
|
+
payloadHash = stableHash([opName, keyExtra ?? "", filter, resultOptions]);
|
|
4009
|
+
}
|
|
4010
|
+
return payloadHash;
|
|
4011
|
+
};
|
|
4012
|
+
const key = useCache ? opts.cache.key(cacheCollectionKey(client.databaseName, physical), getPayloadHash()) : void 0;
|
|
3345
4013
|
const fetchAndCheck = () => executeWrapped().then((value) => {
|
|
3346
4014
|
checkDrift(value);
|
|
3347
4015
|
return value;
|
|
@@ -3360,10 +4028,11 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3360
4028
|
return fetchAndCheck();
|
|
3361
4029
|
};
|
|
3362
4030
|
if (shouldDedupe) {
|
|
3363
|
-
const
|
|
3364
|
-
|
|
4031
|
+
const colKey = cacheCollectionKey(client.databaseName, physical);
|
|
4032
|
+
const gen = opts.cache !== void 0 ? opts.cache.versionOf(colKey) : "";
|
|
4033
|
+
return await opts.inFlight.run(`${colKey}|${gen}|${getPayloadHash()}`, runOnce);
|
|
3365
4034
|
}
|
|
3366
|
-
return runOnce();
|
|
4035
|
+
return await runOnce();
|
|
3367
4036
|
};
|
|
3368
4037
|
const normalizeFindOptions = (options) => {
|
|
3369
4038
|
if (!options) return {};
|
|
@@ -3422,7 +4091,10 @@ var makeReadOps = (ctx) => {
|
|
|
3422
4091
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3423
4092
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3424
4093
|
};
|
|
3425
|
-
return coll(collection).findOne(
|
|
4094
|
+
return await coll(collection).findOne(
|
|
4095
|
+
toDriverFilter(f),
|
|
4096
|
+
opts2
|
|
4097
|
+
);
|
|
3426
4098
|
},
|
|
3427
4099
|
"one"
|
|
3428
4100
|
);
|
|
@@ -3516,7 +4188,7 @@ var makeReadOps = (ctx) => {
|
|
|
3516
4188
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3517
4189
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3518
4190
|
};
|
|
3519
|
-
return coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
4191
|
+
return await coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
3520
4192
|
},
|
|
3521
4193
|
"none"
|
|
3522
4194
|
);
|
|
@@ -3541,21 +4213,24 @@ var makeReadOps = (ctx) => {
|
|
|
3541
4213
|
opts2
|
|
3542
4214
|
);
|
|
3543
4215
|
},
|
|
3544
|
-
"none"
|
|
4216
|
+
"none",
|
|
4217
|
+
String(field)
|
|
3545
4218
|
);
|
|
3546
4219
|
};
|
|
3547
|
-
const estimatedDocumentCount = async (collection, options) =>
|
|
4220
|
+
const estimatedDocumentCount = async (collection, options) => read(
|
|
3548
4221
|
collection,
|
|
3549
4222
|
"mongo.estimatedDocumentCount",
|
|
4223
|
+
{},
|
|
4224
|
+
options,
|
|
3550
4225
|
async (r) => {
|
|
3551
4226
|
const opts2 = {
|
|
3552
4227
|
...r.driverOpts,
|
|
3553
4228
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
3554
4229
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {}
|
|
3555
4230
|
};
|
|
3556
|
-
return coll(collection).estimatedDocumentCount(opts2);
|
|
4231
|
+
return await coll(collection).estimatedDocumentCount(opts2);
|
|
3557
4232
|
},
|
|
3558
|
-
|
|
4233
|
+
"none"
|
|
3559
4234
|
);
|
|
3560
4235
|
return {
|
|
3561
4236
|
getOne,
|
|
@@ -3592,7 +4267,7 @@ var makeWatchOps = (ctx) => {
|
|
|
3592
4267
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3593
4268
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3594
4269
|
};
|
|
3595
|
-
return execute(opts);
|
|
4270
|
+
return await execute(opts);
|
|
3596
4271
|
},
|
|
3597
4272
|
opName === "mongo.getOne" ? "one" : opName === "mongo.findMany" ? "many" : "none"
|
|
3598
4273
|
)
|
|
@@ -3609,6 +4284,20 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3609
4284
|
const { opts } = ctx;
|
|
3610
4285
|
const { coll, toDriverFilter, writeRun, invalidate, stampCreate, stampUpdate, stampReplace } = ctx;
|
|
3611
4286
|
const { getOne } = readOps;
|
|
4287
|
+
const safeAfterHooks = async (collection, name, hookCtx) => {
|
|
4288
|
+
try {
|
|
4289
|
+
await runHooks(opts.hooks, collection, name, hookCtx);
|
|
4290
|
+
} catch (err) {
|
|
4291
|
+
ctx.logger.error?.(
|
|
4292
|
+
{
|
|
4293
|
+
collection,
|
|
4294
|
+
hook: name,
|
|
4295
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4296
|
+
},
|
|
4297
|
+
`post-commit ${name} hook failed; DB write is already committed`
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
3612
4301
|
const insertOne = async (collection, doc, options) => {
|
|
3613
4302
|
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
3614
4303
|
collection: String(collection),
|
|
@@ -3623,28 +4312,32 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3623
4312
|
...r.driverOpts,
|
|
3624
4313
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3625
4314
|
};
|
|
3626
|
-
return coll(collection).insertOne(
|
|
4315
|
+
return await coll(collection).insertOne(
|
|
4316
|
+
doc,
|
|
4317
|
+
insertOpts
|
|
4318
|
+
);
|
|
3627
4319
|
},
|
|
3628
|
-
options
|
|
4320
|
+
options,
|
|
4321
|
+
{ doc }
|
|
3629
4322
|
);
|
|
3630
4323
|
invalidate(collection);
|
|
3631
|
-
await
|
|
4324
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3632
4325
|
collection: String(collection),
|
|
3633
4326
|
doc
|
|
3634
4327
|
});
|
|
3635
4328
|
return result;
|
|
3636
4329
|
};
|
|
3637
4330
|
const insertMany = async (collection, docs, options) => {
|
|
3638
|
-
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
3639
|
-
collection: String(collection),
|
|
3640
|
-
docs
|
|
3641
|
-
});
|
|
3642
|
-
for (const d of docs) stampCreate(collection, d);
|
|
3643
4331
|
if (docs.length > MAX_BATCH_OPS) {
|
|
3644
4332
|
throw new BadRequest(
|
|
3645
4333
|
`insertMany: too many docs (${docs.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
3646
4334
|
);
|
|
3647
4335
|
}
|
|
4336
|
+
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
4337
|
+
collection: String(collection),
|
|
4338
|
+
docs
|
|
4339
|
+
});
|
|
4340
|
+
for (const d of docs) stampCreate(collection, d);
|
|
3648
4341
|
const result = await writeRun(
|
|
3649
4342
|
collection,
|
|
3650
4343
|
"mongo.insertMany",
|
|
@@ -3658,10 +4351,11 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3658
4351
|
bulkOpts
|
|
3659
4352
|
);
|
|
3660
4353
|
},
|
|
3661
|
-
options
|
|
4354
|
+
options,
|
|
4355
|
+
{ docs }
|
|
3662
4356
|
);
|
|
3663
4357
|
invalidate(collection);
|
|
3664
|
-
await
|
|
4358
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3665
4359
|
collection: String(collection),
|
|
3666
4360
|
docs
|
|
3667
4361
|
});
|
|
@@ -3687,16 +4381,21 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3687
4381
|
updateOpts
|
|
3688
4382
|
);
|
|
3689
4383
|
},
|
|
3690
|
-
options
|
|
4384
|
+
options,
|
|
4385
|
+
{ filter, update: effective }
|
|
3691
4386
|
);
|
|
3692
4387
|
invalidate(collection);
|
|
3693
|
-
await
|
|
4388
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
3694
4389
|
collection: String(collection),
|
|
3695
4390
|
filter
|
|
3696
4391
|
});
|
|
3697
4392
|
return result;
|
|
3698
4393
|
};
|
|
3699
4394
|
const updateMany = async (collection, filter, update, options) => {
|
|
4395
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4396
|
+
collection: String(collection),
|
|
4397
|
+
filter
|
|
4398
|
+
});
|
|
3700
4399
|
const effective = stampUpdate(collection, update);
|
|
3701
4400
|
const result = await writeRun(
|
|
3702
4401
|
collection,
|
|
@@ -3712,12 +4411,21 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3712
4411
|
updateOpts
|
|
3713
4412
|
);
|
|
3714
4413
|
},
|
|
3715
|
-
options
|
|
4414
|
+
options,
|
|
4415
|
+
{ filter, update: effective }
|
|
3716
4416
|
);
|
|
3717
4417
|
invalidate(collection);
|
|
4418
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4419
|
+
collection: String(collection),
|
|
4420
|
+
filter
|
|
4421
|
+
});
|
|
3718
4422
|
return result;
|
|
3719
4423
|
};
|
|
3720
4424
|
const findOneAndUpdate = async (collection, filter, update, options) => {
|
|
4425
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4426
|
+
collection: String(collection),
|
|
4427
|
+
filter
|
|
4428
|
+
});
|
|
3721
4429
|
const effective = stampUpdate(collection, update);
|
|
3722
4430
|
const result = await writeRun(
|
|
3723
4431
|
collection,
|
|
@@ -3737,12 +4445,21 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3737
4445
|
fOpts
|
|
3738
4446
|
);
|
|
3739
4447
|
},
|
|
3740
|
-
options
|
|
4448
|
+
options,
|
|
4449
|
+
{ filter, update: effective }
|
|
3741
4450
|
);
|
|
3742
4451
|
invalidate(collection);
|
|
4452
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4453
|
+
collection: String(collection),
|
|
4454
|
+
filter
|
|
4455
|
+
});
|
|
3743
4456
|
return result;
|
|
3744
4457
|
};
|
|
3745
4458
|
const findOneAndReplace = async (collection, filter, replacement, options) => {
|
|
4459
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4460
|
+
collection: String(collection),
|
|
4461
|
+
filter
|
|
4462
|
+
});
|
|
3746
4463
|
const effective = stampReplace(collection, replacement);
|
|
3747
4464
|
const result = await writeRun(
|
|
3748
4465
|
collection,
|
|
@@ -3762,12 +4479,21 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3762
4479
|
fOpts
|
|
3763
4480
|
);
|
|
3764
4481
|
},
|
|
3765
|
-
options
|
|
4482
|
+
options,
|
|
4483
|
+
{ filter, replacement: effective }
|
|
3766
4484
|
);
|
|
3767
4485
|
invalidate(collection);
|
|
4486
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4487
|
+
collection: String(collection),
|
|
4488
|
+
filter
|
|
4489
|
+
});
|
|
3768
4490
|
return result;
|
|
3769
4491
|
};
|
|
3770
4492
|
const replaceOne = async (collection, filter, replacement, options) => {
|
|
4493
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4494
|
+
collection: String(collection),
|
|
4495
|
+
filter
|
|
4496
|
+
});
|
|
3771
4497
|
const result = await writeRun(
|
|
3772
4498
|
collection,
|
|
3773
4499
|
"mongo.replaceOne",
|
|
@@ -3782,9 +4508,14 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3782
4508
|
repOpts
|
|
3783
4509
|
);
|
|
3784
4510
|
},
|
|
3785
|
-
options
|
|
4511
|
+
options,
|
|
4512
|
+
{ filter, replacement }
|
|
3786
4513
|
);
|
|
3787
4514
|
invalidate(collection);
|
|
4515
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4516
|
+
collection: String(collection),
|
|
4517
|
+
filter
|
|
4518
|
+
});
|
|
3788
4519
|
return result;
|
|
3789
4520
|
};
|
|
3790
4521
|
const deleteOne = async (collection, filter, options) => {
|
|
@@ -3800,18 +4531,23 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3800
4531
|
...r.driverOpts,
|
|
3801
4532
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3802
4533
|
};
|
|
3803
|
-
return coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
4534
|
+
return await coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
3804
4535
|
},
|
|
3805
|
-
options
|
|
4536
|
+
options,
|
|
4537
|
+
{ filter }
|
|
3806
4538
|
);
|
|
3807
4539
|
invalidate(collection);
|
|
3808
|
-
await
|
|
4540
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
3809
4541
|
collection: String(collection),
|
|
3810
4542
|
filter
|
|
3811
4543
|
});
|
|
3812
4544
|
return result;
|
|
3813
4545
|
};
|
|
3814
4546
|
const deleteMany = async (collection, filter, options) => {
|
|
4547
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4548
|
+
collection: String(collection),
|
|
4549
|
+
filter
|
|
4550
|
+
});
|
|
3815
4551
|
const result = await writeRun(
|
|
3816
4552
|
collection,
|
|
3817
4553
|
"mongo.deleteMany",
|
|
@@ -3820,14 +4556,23 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3820
4556
|
...r.driverOpts,
|
|
3821
4557
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3822
4558
|
};
|
|
3823
|
-
return coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
4559
|
+
return await coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
3824
4560
|
},
|
|
3825
|
-
options
|
|
4561
|
+
options,
|
|
4562
|
+
{ filter }
|
|
3826
4563
|
);
|
|
3827
4564
|
invalidate(collection);
|
|
4565
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4566
|
+
collection: String(collection),
|
|
4567
|
+
filter
|
|
4568
|
+
});
|
|
3828
4569
|
return result;
|
|
3829
4570
|
};
|
|
3830
4571
|
const findOneAndDelete = async (collection, filter, options) => {
|
|
4572
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4573
|
+
collection: String(collection),
|
|
4574
|
+
filter
|
|
4575
|
+
});
|
|
3831
4576
|
const result = await writeRun(
|
|
3832
4577
|
collection,
|
|
3833
4578
|
"mongo.findOneAndDelete",
|
|
@@ -3843,13 +4588,25 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3843
4588
|
fOpts
|
|
3844
4589
|
);
|
|
3845
4590
|
},
|
|
3846
|
-
options
|
|
4591
|
+
options,
|
|
4592
|
+
{ filter }
|
|
3847
4593
|
);
|
|
3848
4594
|
invalidate(collection);
|
|
4595
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4596
|
+
collection: String(collection),
|
|
4597
|
+
filter
|
|
4598
|
+
});
|
|
3849
4599
|
return result;
|
|
3850
4600
|
};
|
|
3851
4601
|
const softDeleteOne = async (collection, filter, options) => {
|
|
4602
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4603
|
+
collection: String(collection),
|
|
4604
|
+
filter
|
|
4605
|
+
});
|
|
3852
4606
|
const active = mergeMongoActiveFilter(true, filter);
|
|
4607
|
+
const effective = stampUpdate(collection, {
|
|
4608
|
+
$set: { deletedAt: /* @__PURE__ */ new Date() }
|
|
4609
|
+
});
|
|
3853
4610
|
const result = await writeRun(
|
|
3854
4611
|
collection,
|
|
3855
4612
|
"mongo.softDeleteOne",
|
|
@@ -3860,16 +4617,25 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3860
4617
|
};
|
|
3861
4618
|
return coll(collection).updateOne(
|
|
3862
4619
|
active,
|
|
3863
|
-
|
|
4620
|
+
effective,
|
|
3864
4621
|
updateOpts
|
|
3865
4622
|
);
|
|
3866
4623
|
},
|
|
3867
|
-
options
|
|
4624
|
+
options,
|
|
4625
|
+
{ filter, update: effective }
|
|
3868
4626
|
);
|
|
3869
4627
|
invalidate(collection);
|
|
4628
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4629
|
+
collection: String(collection),
|
|
4630
|
+
filter
|
|
4631
|
+
});
|
|
3870
4632
|
return result;
|
|
3871
4633
|
};
|
|
3872
4634
|
const upsert = async (collection, filter, update, options) => {
|
|
4635
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4636
|
+
collection: String(collection),
|
|
4637
|
+
filter
|
|
4638
|
+
});
|
|
3873
4639
|
const formatted = stampUpdate(collection, update);
|
|
3874
4640
|
const result = await writeRun(
|
|
3875
4641
|
collection,
|
|
@@ -3880,14 +4646,24 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3880
4646
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
3881
4647
|
upsert: true
|
|
3882
4648
|
};
|
|
3883
|
-
return coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
4649
|
+
return await coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
3884
4650
|
},
|
|
3885
|
-
options
|
|
4651
|
+
options,
|
|
4652
|
+
{ filter, update: formatted }
|
|
3886
4653
|
);
|
|
3887
4654
|
invalidate(collection);
|
|
4655
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4656
|
+
collection: String(collection),
|
|
4657
|
+
filter
|
|
4658
|
+
});
|
|
3888
4659
|
return result;
|
|
3889
4660
|
};
|
|
3890
4661
|
const bulkUpsert = async (collection, operations, options) => {
|
|
4662
|
+
if (operations.length > MAX_BATCH_OPS) {
|
|
4663
|
+
throw new BadRequest(
|
|
4664
|
+
`bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
3891
4667
|
const writes = operations.map((op) => ({
|
|
3892
4668
|
updateOne: {
|
|
3893
4669
|
filter: op.filter,
|
|
@@ -3895,11 +4671,6 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3895
4671
|
upsert: op.upsert ?? true
|
|
3896
4672
|
}
|
|
3897
4673
|
}));
|
|
3898
|
-
if (operations.length > MAX_BATCH_OPS) {
|
|
3899
|
-
throw new BadRequest(
|
|
3900
|
-
`bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
3901
|
-
);
|
|
3902
|
-
}
|
|
3903
4674
|
const result = await writeRun(
|
|
3904
4675
|
collection,
|
|
3905
4676
|
"mongo.bulkUpsert",
|
|
@@ -3908,9 +4679,10 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3908
4679
|
...r.driverOpts,
|
|
3909
4680
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3910
4681
|
};
|
|
3911
|
-
return coll(collection).bulkWrite(writes, bulkOpts);
|
|
4682
|
+
return await coll(collection).bulkWrite(writes, bulkOpts);
|
|
3912
4683
|
},
|
|
3913
|
-
options
|
|
4684
|
+
options,
|
|
4685
|
+
{ operations }
|
|
3914
4686
|
);
|
|
3915
4687
|
invalidate(collection);
|
|
3916
4688
|
return result;
|
|
@@ -3929,9 +4701,10 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3929
4701
|
...r.driverOpts,
|
|
3930
4702
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3931
4703
|
};
|
|
3932
|
-
return coll(collection).bulkWrite(operations, bulkOpts);
|
|
4704
|
+
return await coll(collection).bulkWrite(operations, bulkOpts);
|
|
3933
4705
|
},
|
|
3934
|
-
options
|
|
4706
|
+
options,
|
|
4707
|
+
{ operations }
|
|
3935
4708
|
);
|
|
3936
4709
|
invalidate(collection);
|
|
3937
4710
|
return result;
|
|
@@ -4001,14 +4774,29 @@ var tag = (value) => {
|
|
|
4001
4774
|
var untag = (tagged) => {
|
|
4002
4775
|
switch (tagged.t) {
|
|
4003
4776
|
case "oid":
|
|
4777
|
+
if (typeof tagged.v !== "string") {
|
|
4778
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 oid value must be a string");
|
|
4779
|
+
}
|
|
4004
4780
|
return new ObjectId(tagged.v);
|
|
4005
4781
|
case "date":
|
|
4782
|
+
if (typeof tagged.v !== "string") {
|
|
4783
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 date value must be a string");
|
|
4784
|
+
}
|
|
4006
4785
|
return new Date(tagged.v);
|
|
4007
4786
|
case "string":
|
|
4787
|
+
if (typeof tagged.v !== "string") {
|
|
4788
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 string value expected");
|
|
4789
|
+
}
|
|
4008
4790
|
return tagged.v;
|
|
4009
4791
|
case "number":
|
|
4792
|
+
if (typeof tagged.v !== "number") {
|
|
4793
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 number value expected");
|
|
4794
|
+
}
|
|
4010
4795
|
return tagged.v;
|
|
4011
4796
|
case "boolean":
|
|
4797
|
+
if (typeof tagged.v !== "boolean") {
|
|
4798
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 boolean value expected");
|
|
4799
|
+
}
|
|
4012
4800
|
return tagged.v;
|
|
4013
4801
|
case "null":
|
|
4014
4802
|
return null;
|
|
@@ -4094,11 +4882,14 @@ var makePaginateCursor = (ctx) => {
|
|
|
4094
4882
|
...r.driverOpts,
|
|
4095
4883
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4096
4884
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4097
|
-
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
4885
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
4886
|
+
...r.sdk.batchSize !== void 0 ? { batchSize: r.sdk.batchSize } : {}
|
|
4098
4887
|
};
|
|
4099
4888
|
return client.collection(resolve(String(collection))).find(filter, findOpts).sort(effectiveSort).limit(limit + 1).toArray();
|
|
4100
4889
|
},
|
|
4101
|
-
config.queryOptions
|
|
4890
|
+
config.queryOptions,
|
|
4891
|
+
false,
|
|
4892
|
+
{ filter, sort: effectiveSort, limit }
|
|
4102
4893
|
);
|
|
4103
4894
|
const hasMore = rows.length > limit;
|
|
4104
4895
|
const page = backwards ? rows.slice(0, limit).reverse() : rows.slice(0, limit);
|
|
@@ -4114,10 +4905,16 @@ var makePaginateCursor = (ctx) => {
|
|
|
4114
4905
|
page
|
|
4115
4906
|
);
|
|
4116
4907
|
const boundary = backwards ? rows[limit] : page[page.length - 1];
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
values
|
|
4120
|
-
|
|
4908
|
+
let nextCursor = null;
|
|
4909
|
+
if (hasMore && boundary !== void 0) {
|
|
4910
|
+
const values = sortKeys.map(([f]) => boundary[f]);
|
|
4911
|
+
if (values.some((v) => v === void 0)) {
|
|
4912
|
+
throw new BadRequest(
|
|
4913
|
+
"paginateCursor: boundary document is missing a sort field \u2014 every sort key must exist on every document for keyset pagination"
|
|
4914
|
+
);
|
|
4915
|
+
}
|
|
4916
|
+
nextCursor = encodeCursor({ sort: config.sort, values });
|
|
4917
|
+
}
|
|
4121
4918
|
return { data: page, nextCursor, hasMore };
|
|
4122
4919
|
};
|
|
4123
4920
|
return { paginateCursor };
|
|
@@ -4132,13 +4929,17 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4132
4929
|
...config.page !== void 0 ? { page: config.page } : {},
|
|
4133
4930
|
...config.limit !== void 0 ? { limit: config.limit } : {}
|
|
4134
4931
|
},
|
|
4135
|
-
config.maxLimit ?? DEFAULT_MAX_LIMIT
|
|
4932
|
+
config.maxLimit ?? DEFAULT_MAX_LIMIT,
|
|
4933
|
+
// Same default as `findMany`/`paginateCursor` — the two pagination
|
|
4934
|
+
// APIs used to disagree silently (10 vs 100).
|
|
4935
|
+
{ limit: DEFAULT_FIND_LIMIT }
|
|
4136
4936
|
);
|
|
4137
4937
|
const dataStages = [
|
|
4138
4938
|
...config.prePipeline ?? [],
|
|
4139
4939
|
...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
|
|
4140
4940
|
{ $skip: normalized.offset },
|
|
4141
4941
|
{ $limit: normalized.limit },
|
|
4942
|
+
...config.projection !== void 0 ? [{ $project: config.projection }] : [],
|
|
4142
4943
|
...config.postPipeline ?? []
|
|
4143
4944
|
];
|
|
4144
4945
|
const pipeline = [
|
|
@@ -4167,17 +4968,19 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4167
4968
|
const row = rows[0];
|
|
4168
4969
|
const data = row?.data ?? [];
|
|
4169
4970
|
const totalCount = row?.totalCount?.[0]?.count ?? 0;
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4971
|
+
if (config.projection === void 0) {
|
|
4972
|
+
checkDocsDrift(
|
|
4973
|
+
{
|
|
4974
|
+
logger,
|
|
4975
|
+
db: dbLabel,
|
|
4976
|
+
drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
|
|
4977
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
4978
|
+
},
|
|
4979
|
+
String(collection),
|
|
4980
|
+
"mongo.paginateFlexible",
|
|
4981
|
+
data
|
|
4982
|
+
);
|
|
4983
|
+
}
|
|
4181
4984
|
return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
|
|
4182
4985
|
}
|
|
4183
4986
|
});
|
|
@@ -4205,42 +5008,70 @@ var makePaginationOps = (client, dbLabel, logger, opts) => {
|
|
|
4205
5008
|
};
|
|
4206
5009
|
|
|
4207
5010
|
// src/service/schema-ops.ts
|
|
4208
|
-
var
|
|
4209
|
-
|
|
4210
|
-
)
|
|
5011
|
+
var optionSignature = (o) => JSON.stringify([
|
|
5012
|
+
Boolean(o?.unique),
|
|
5013
|
+
Boolean(o?.sparse),
|
|
5014
|
+
typeof o?.expireAfterSeconds === "number" ? o.expireAfterSeconds : null
|
|
5015
|
+
]);
|
|
5016
|
+
var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
|
|
4211
5017
|
var makeSchemaOps = (client, _logger, opts) => {
|
|
5018
|
+
const readCollectionValidator = async (physical) => {
|
|
5019
|
+
const info = await client.listCollections({ name: physical }).next();
|
|
5020
|
+
return info?.options?.validator;
|
|
5021
|
+
};
|
|
5022
|
+
const ensureDeclaredIndexes = async (collection, physical, def) => {
|
|
5023
|
+
const declared = def?.indexes ?? [];
|
|
5024
|
+
if (declared.length === 0) return;
|
|
5025
|
+
const handle = client.collection(physical);
|
|
5026
|
+
const existing = new Set(
|
|
5027
|
+
(await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_").map((i) => indexKeyOf(i.key, i))
|
|
5028
|
+
);
|
|
5029
|
+
for (const index of declared) {
|
|
5030
|
+
if (existing.has(indexKeyOf(index.key, index.options))) continue;
|
|
5031
|
+
try {
|
|
5032
|
+
await handle.createIndex(index.key, index.options);
|
|
5033
|
+
} catch (err) {
|
|
5034
|
+
if (err?.code === 85) {
|
|
5035
|
+
throw new Error(
|
|
5036
|
+
`Cannot apply declared index on "${collection}": an index with the same name/key already exists with different options. Run db.syncIndexes("${collection}") to reconcile index drift.`,
|
|
5037
|
+
{ cause: err }
|
|
5038
|
+
);
|
|
5039
|
+
}
|
|
5040
|
+
throw err;
|
|
5041
|
+
}
|
|
5042
|
+
}
|
|
5043
|
+
};
|
|
4212
5044
|
const createSchema = async (collection, createCollectionOptions) => {
|
|
4213
5045
|
const physical = opts.resolveCollectionName(collection);
|
|
4214
5046
|
const def = opts.getDefinition(collection);
|
|
4215
|
-
const
|
|
5047
|
+
const createOptions = {
|
|
5048
|
+
...createCollectionOptions ?? {},
|
|
5049
|
+
...def?.schema ? { validator: toMongoValidator(def.schema) } : {}
|
|
5050
|
+
};
|
|
5051
|
+
let created = true;
|
|
4216
5052
|
try {
|
|
4217
|
-
await client.createCollection(physical,
|
|
4218
|
-
...validator ? { validator } : {},
|
|
4219
|
-
...createCollectionOptions ?? {}
|
|
4220
|
-
});
|
|
5053
|
+
await client.createCollection(physical, createOptions);
|
|
4221
5054
|
} catch (err) {
|
|
4222
|
-
if (err?.code
|
|
4223
|
-
|
|
4224
|
-
physicalCollection: physical
|
|
4225
|
-
});
|
|
4226
|
-
}
|
|
4227
|
-
throw err;
|
|
5055
|
+
if (err?.code !== 48) throw err;
|
|
5056
|
+
created = false;
|
|
4228
5057
|
}
|
|
4229
|
-
|
|
4230
|
-
if (indexes.length > 0) {
|
|
5058
|
+
if (created) {
|
|
4231
5059
|
try {
|
|
4232
|
-
await
|
|
4233
|
-
indexes.map((index) => ({
|
|
4234
|
-
key: index.key,
|
|
4235
|
-
...index.options ?? {}
|
|
4236
|
-
}))
|
|
4237
|
-
);
|
|
5060
|
+
await ensureDeclaredIndexes(collection, physical, def);
|
|
4238
5061
|
} catch (err) {
|
|
4239
5062
|
await client.dropCollection(physical).catch(() => {
|
|
4240
5063
|
});
|
|
4241
5064
|
throw err;
|
|
4242
5065
|
}
|
|
5066
|
+
return;
|
|
4243
5067
|
}
|
|
5068
|
+
if (createOptions.validator) {
|
|
5069
|
+
const current = await readCollectionValidator(physical);
|
|
5070
|
+
if (JSON.stringify(current) !== JSON.stringify(createOptions.validator)) {
|
|
5071
|
+
await client.command({ collMod: physical, validator: createOptions.validator });
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
await ensureDeclaredIndexes(collection, physical, def);
|
|
4244
5075
|
};
|
|
4245
5076
|
const updateSchema = async (collection, schema) => {
|
|
4246
5077
|
const physical = opts.resolveCollectionName(collection);
|
|
@@ -4261,9 +5092,11 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4261
5092
|
const handle = client.collection(physical);
|
|
4262
5093
|
const existing = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4263
5094
|
const existingByKey = new Map(
|
|
4264
|
-
existing.map((i) => [indexKeyOf(i.key), i.name])
|
|
5095
|
+
existing.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5096
|
+
);
|
|
5097
|
+
const declaredByKey = new Map(
|
|
5098
|
+
declared.map((d) => [indexKeyOf(d.key, d.options), d])
|
|
4265
5099
|
);
|
|
4266
|
-
const declaredByKey = new Map(declared.map((d) => [indexKeyOf(d.key), d]));
|
|
4267
5100
|
const created = [];
|
|
4268
5101
|
const dropped = [];
|
|
4269
5102
|
for (const [key] of declaredByKey) {
|
|
@@ -4275,7 +5108,9 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4275
5108
|
}
|
|
4276
5109
|
}
|
|
4277
5110
|
const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4278
|
-
const afterByKey = new Map(
|
|
5111
|
+
const afterByKey = new Map(
|
|
5112
|
+
after.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5113
|
+
);
|
|
4279
5114
|
for (const [key, spec] of declaredByKey) {
|
|
4280
5115
|
if (!existingByKey.has(key)) {
|
|
4281
5116
|
created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
|
|
@@ -4294,16 +5129,22 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4294
5129
|
|
|
4295
5130
|
// src/service/transaction.ts
|
|
4296
5131
|
var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
5132
|
+
let warnedUnsupported = false;
|
|
5133
|
+
let journalIndexReady = false;
|
|
5134
|
+
const MIGRATE_LEASE_MS = 12e4;
|
|
4297
5135
|
const transaction = async (fn, options) => {
|
|
4298
5136
|
const session = client.startSession();
|
|
4299
5137
|
try {
|
|
4300
5138
|
return await session.withTransaction(async () => fn(session), options);
|
|
4301
5139
|
} catch (err) {
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
5140
|
+
if (isTransactionUnsupportedError(err)) {
|
|
5141
|
+
if (!warnedUnsupported) {
|
|
5142
|
+
warnedUnsupported = true;
|
|
5143
|
+
logger.warn?.(
|
|
5144
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
5145
|
+
"transactions unsupported on this deployment, falling back to non-transactional"
|
|
5146
|
+
);
|
|
5147
|
+
}
|
|
4307
5148
|
return fn(null);
|
|
4308
5149
|
}
|
|
4309
5150
|
if (opts.wrapMongoErrors) {
|
|
@@ -4315,34 +5156,105 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
4315
5156
|
}
|
|
4316
5157
|
};
|
|
4317
5158
|
const migrate = async (name, fn, filePath) => {
|
|
4318
|
-
const journal = handle.collection(
|
|
4319
|
-
|
|
4320
|
-
|
|
5159
|
+
const journal = handle.collection(opts.migrationCollectionPhysical);
|
|
5160
|
+
if (!journalIndexReady) {
|
|
5161
|
+
try {
|
|
5162
|
+
await journal.createIndex({ name: 1 }, { unique: true });
|
|
5163
|
+
} catch {
|
|
5164
|
+
}
|
|
5165
|
+
journalIndexReady = true;
|
|
5166
|
+
}
|
|
5167
|
+
const isDupKeyError = (err) => {
|
|
5168
|
+
const code = err?.code;
|
|
5169
|
+
if (code === 11e3 || code === 11001) return true;
|
|
5170
|
+
return /E11000 duplicate key/.test(String(err?.message ?? ""));
|
|
5171
|
+
};
|
|
4321
5172
|
await transaction(async (session) => {
|
|
4322
5173
|
const sessionOpts = session ? { session } : {};
|
|
4323
|
-
const
|
|
4324
|
-
|
|
5174
|
+
const now = /* @__PURE__ */ new Date();
|
|
5175
|
+
const expiresAt = new Date(Date.now() + MIGRATE_LEASE_MS);
|
|
5176
|
+
const stole = await journal.findOneAndUpdate(
|
|
4325
5177
|
{
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
5178
|
+
name,
|
|
5179
|
+
status: { $exists: true, $nin: ["applied"] },
|
|
5180
|
+
$or: [
|
|
5181
|
+
{ expiresAt: { $lt: now } },
|
|
5182
|
+
{
|
|
5183
|
+
expiresAt: { $exists: false },
|
|
5184
|
+
appliedAt: { $lt: new Date(now.getTime() - MIGRATE_LEASE_MS) }
|
|
5185
|
+
}
|
|
5186
|
+
]
|
|
5187
|
+
},
|
|
5188
|
+
{
|
|
5189
|
+
$set: {
|
|
5190
|
+
status: "running",
|
|
5191
|
+
appliedAt: now,
|
|
5192
|
+
expiresAt,
|
|
4329
5193
|
...filePath ? { file: filePath } : {}
|
|
4330
5194
|
}
|
|
4331
5195
|
},
|
|
4332
|
-
{
|
|
5196
|
+
{ returnDocument: "before", ...sessionOpts }
|
|
4333
5197
|
);
|
|
4334
|
-
if (
|
|
4335
|
-
|
|
5198
|
+
if (stole !== null) {
|
|
5199
|
+
await runClaimed(session);
|
|
4336
5200
|
return;
|
|
4337
5201
|
}
|
|
5202
|
+
try {
|
|
5203
|
+
await journal.insertOne(
|
|
5204
|
+
{
|
|
5205
|
+
name,
|
|
5206
|
+
appliedAt: now,
|
|
5207
|
+
status: "running",
|
|
5208
|
+
expiresAt,
|
|
5209
|
+
...filePath ? { file: filePath } : {}
|
|
5210
|
+
},
|
|
5211
|
+
sessionOpts
|
|
5212
|
+
);
|
|
5213
|
+
} catch (err) {
|
|
5214
|
+
if (!isDupKeyError(err)) throw err;
|
|
5215
|
+
const existing = await journal.findOne(
|
|
5216
|
+
{ name },
|
|
5217
|
+
{ projection: { status: 1 }, ...sessionOpts }
|
|
5218
|
+
);
|
|
5219
|
+
const status = existing?.status;
|
|
5220
|
+
if (status === void 0 || status === "applied") {
|
|
5221
|
+
logger.info({ name }, "migration already applied, skipping");
|
|
5222
|
+
} else {
|
|
5223
|
+
logger.warn({ name }, "migration claim held by another runner, skipping");
|
|
5224
|
+
}
|
|
5225
|
+
return;
|
|
5226
|
+
}
|
|
5227
|
+
await runClaimed(session);
|
|
5228
|
+
});
|
|
5229
|
+
async function runClaimed(session) {
|
|
5230
|
+
const sessionOpts = session ? { session } : {};
|
|
5231
|
+
const heartbeat = session === null ? setInterval(() => {
|
|
5232
|
+
void journal.updateOne(
|
|
5233
|
+
{ name, status: "running" },
|
|
5234
|
+
{ $set: { expiresAt: new Date(Date.now() + MIGRATE_LEASE_MS) } }
|
|
5235
|
+
).catch(() => {
|
|
5236
|
+
});
|
|
5237
|
+
}, MIGRATE_LEASE_MS / 2) : void 0;
|
|
5238
|
+
heartbeat?.unref?.();
|
|
4338
5239
|
try {
|
|
4339
5240
|
await fn(session);
|
|
5241
|
+
const res = await journal.updateOne(
|
|
5242
|
+
{ name, status: "running" },
|
|
5243
|
+
{ $set: { status: "applied" } },
|
|
5244
|
+
sessionOpts
|
|
5245
|
+
);
|
|
5246
|
+
if (res?.matchedCount === 0) {
|
|
5247
|
+
await journal.insertOne({ name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }, sessionOpts).catch(() => {
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
4340
5250
|
} catch (err) {
|
|
4341
5251
|
await journal.deleteOne({ name }, sessionOpts).catch(() => {
|
|
4342
5252
|
});
|
|
4343
5253
|
throw err;
|
|
5254
|
+
} finally {
|
|
5255
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
4344
5256
|
}
|
|
4345
|
-
}
|
|
5257
|
+
}
|
|
4346
5258
|
};
|
|
4347
5259
|
return { transaction, migrate };
|
|
4348
5260
|
};
|
|
@@ -4487,11 +5399,27 @@ var createMongoService = (dbClients, configOverrides = {}) => {
|
|
|
4487
5399
|
};
|
|
4488
5400
|
|
|
4489
5401
|
// src/shared/merge-filters.ts
|
|
5402
|
+
var isOperatorDoc = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4490
5403
|
var mergeMongoFilters = (...parts) => {
|
|
4491
5404
|
const out = {};
|
|
4492
5405
|
for (const part of parts) {
|
|
4493
5406
|
if (!part) continue;
|
|
4494
|
-
Object.
|
|
5407
|
+
for (const [key, value] of Object.entries(part)) {
|
|
5408
|
+
if (!(key in out)) {
|
|
5409
|
+
out[key] = value;
|
|
5410
|
+
continue;
|
|
5411
|
+
}
|
|
5412
|
+
if (key === "$and" || key === "$or") {
|
|
5413
|
+
const prev2 = out[key];
|
|
5414
|
+
out[key] = [
|
|
5415
|
+
...Array.isArray(prev2) ? prev2 : [prev2],
|
|
5416
|
+
...Array.isArray(value) ? value : [value]
|
|
5417
|
+
];
|
|
5418
|
+
continue;
|
|
5419
|
+
}
|
|
5420
|
+
const prev = out[key];
|
|
5421
|
+
out[key] = isOperatorDoc(prev) && isOperatorDoc(value) ? { ...prev, ...value } : value;
|
|
5422
|
+
}
|
|
4495
5423
|
}
|
|
4496
5424
|
return out;
|
|
4497
5425
|
};
|
|
@@ -4570,6 +5498,6 @@ var createCachedAsyncFactory = (factory, options = {}) => {
|
|
|
4570
5498
|
};
|
|
4571
5499
|
};
|
|
4572
5500
|
|
|
4573
|
-
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 };
|
|
5501
|
+
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 };
|
|
4574
5502
|
//# sourceMappingURL=index.js.map
|
|
4575
5503
|
//# sourceMappingURL=index.js.map
|