@ignex/ninox 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/dist/index.d.ts +200 -66
- package/dist/index.js +1204 -340
- 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;
|
|
388
500
|
}
|
|
389
|
-
|
|
501
|
+
if (value instanceof Map) {
|
|
502
|
+
let sum2 = OVERHEAD_PER_KEY * value.size;
|
|
503
|
+
for (const [k, v] of value) sum2 += estimateSize(k, seen) + visit(v);
|
|
504
|
+
return sum2;
|
|
505
|
+
}
|
|
506
|
+
if (value instanceof Set) {
|
|
507
|
+
let sum2 = OVERHEAD_PER_KEY * value.size;
|
|
508
|
+
for (const v of value) sum2 += visit(v);
|
|
509
|
+
return sum2;
|
|
510
|
+
}
|
|
511
|
+
if (ArrayBuffer.isView(value)) {
|
|
512
|
+
return value.byteLength + OVERHEAD_PER_KEY;
|
|
513
|
+
}
|
|
514
|
+
let sum = 0;
|
|
515
|
+
for (const [k, v] of Object.entries(value)) {
|
|
516
|
+
sum += k.length * 2 + OVERHEAD_PER_KEY + visit(v);
|
|
517
|
+
}
|
|
518
|
+
return sum;
|
|
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
|
-
|
|
2530
|
-
const
|
|
2531
|
-
|
|
3048
|
+
const out = docs.map((doc) => ({ ...doc }));
|
|
3049
|
+
if (out.length === 0 || relations.length === 0) return out;
|
|
3050
|
+
const loaderCache = /* @__PURE__ */ new Map();
|
|
3051
|
+
const loaderFor = (collection, foreignField) => {
|
|
3052
|
+
const cacheKey = `${collection}\0${foreignField}`;
|
|
3053
|
+
let loader = loaderCache.get(cacheKey);
|
|
3054
|
+
if (!loader) {
|
|
3055
|
+
loader = createForeignKeyLoader(deps2, collection, foreignField, options);
|
|
3056
|
+
loaderCache.set(cacheKey, loader);
|
|
3057
|
+
}
|
|
3058
|
+
return loader;
|
|
3059
|
+
};
|
|
3060
|
+
const loadIndex = async (collection, foreignField, keys) => {
|
|
3061
|
+
const values = await loaderFor(collection, foreignField).loadMany(keys);
|
|
3062
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3063
|
+
keys.forEach((key, i) => {
|
|
3064
|
+
byKey.set(canonicalKey(key), values[i] ?? []);
|
|
3065
|
+
});
|
|
3066
|
+
return byKey;
|
|
3067
|
+
};
|
|
3068
|
+
await Promise.all(
|
|
3069
|
+
relations.map(async (relation) => {
|
|
3070
|
+
const target = relation.foreignField ?? "_id";
|
|
3071
|
+
const sourceKeys = distinctKeys(out.map((doc) => doc[relation.localField]));
|
|
3072
|
+
if (relation.type === "belongsTo") {
|
|
3073
|
+
const byKey = await loadIndex(relation.collection, target, sourceKeys);
|
|
3074
|
+
for (const doc of out) {
|
|
3075
|
+
const key = doc[relation.localField];
|
|
3076
|
+
const matches = key === void 0 || key === null ? void 0 : byKey.get(canonicalKey(key));
|
|
3077
|
+
doc[relation.as] = matches?.[0] ?? (relation.optional ? void 0 : null);
|
|
3078
|
+
}
|
|
3079
|
+
} else if (relation.type === "hasMany") {
|
|
3080
|
+
const byKey = await loadIndex(relation.collection, target, sourceKeys);
|
|
3081
|
+
for (const doc of out) {
|
|
3082
|
+
const key = doc[relation.localField];
|
|
3083
|
+
doc[relation.as] = key === void 0 || key === null ? [] : byKey.get(canonicalKey(key)) ?? [];
|
|
3084
|
+
}
|
|
3085
|
+
} else {
|
|
3086
|
+
const throughByKey = await loadIndex(
|
|
3087
|
+
relation.through.collection,
|
|
3088
|
+
relation.through.localField,
|
|
3089
|
+
sourceKeys
|
|
3090
|
+
);
|
|
3091
|
+
const joinDocs = [...throughByKey.values()].flat();
|
|
3092
|
+
const targetKeys = distinctKeys(
|
|
3093
|
+
joinDocs.map((join3) => join3[relation.through.foreignField])
|
|
2532
3094
|
);
|
|
2533
|
-
|
|
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
|
};
|
|
@@ -3010,10 +3586,12 @@ var createCachedAggregate = (ctx) => {
|
|
|
3010
3586
|
return runDriver();
|
|
3011
3587
|
};
|
|
3012
3588
|
if (shouldDedupe) {
|
|
3013
|
-
const
|
|
3014
|
-
|
|
3589
|
+
const primaryKey = cacheCollectionKey(client.databaseName, physical);
|
|
3590
|
+
const gen = cache !== void 0 ? cache.versionOf(primaryKey) : "";
|
|
3591
|
+
const dedupeKey = `${primaryKey}|${gen}|${opName}|${stableHash([pipeline, options])}`;
|
|
3592
|
+
return await opts.inFlight.run(dedupeKey, runOnce);
|
|
3015
3593
|
}
|
|
3016
|
-
return runOnce();
|
|
3594
|
+
return await runOnce();
|
|
3017
3595
|
};
|
|
3018
3596
|
};
|
|
3019
3597
|
|
|
@@ -3235,6 +3813,54 @@ var makeAggregationOps = (client, dbLabel, logger, opts) => {
|
|
|
3235
3813
|
};
|
|
3236
3814
|
};
|
|
3237
3815
|
|
|
3816
|
+
// src/schema/apply-defaults.ts
|
|
3817
|
+
var cloneDefault = (value) => {
|
|
3818
|
+
if (value === null || typeof value !== "object") return value;
|
|
3819
|
+
if (typeof structuredClone === "function") return structuredClone(value);
|
|
3820
|
+
return Array.isArray(value) ? [...value] : { ...value };
|
|
3821
|
+
};
|
|
3822
|
+
var applySchemaDefaults = (schema, doc) => {
|
|
3823
|
+
if (!schema || doc === null || typeof doc !== "object" || Array.isArray(doc)) return;
|
|
3824
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3825
|
+
if (key in doc && doc[key] !== void 0) {
|
|
3826
|
+
if (prop.kind === "object" && doc[key] !== null && typeof doc[key] === "object" && !Array.isArray(doc[key])) {
|
|
3827
|
+
applySchemaDefaults(prop, doc[key]);
|
|
3828
|
+
}
|
|
3829
|
+
continue;
|
|
3830
|
+
}
|
|
3831
|
+
if (prop.flags.hasDefault) {
|
|
3832
|
+
doc[key] = cloneDefault(prop.flags.defaultValue);
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
|
|
3837
|
+
// src/utils/omit-undefined.ts
|
|
3838
|
+
var stripUndefinedKeys = (doc) => {
|
|
3839
|
+
for (const key of Object.keys(doc)) {
|
|
3840
|
+
if (doc[key] === void 0) {
|
|
3841
|
+
delete doc[key];
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
return doc;
|
|
3845
|
+
};
|
|
3846
|
+
var stripUndefinedFromUpdate = (update) => {
|
|
3847
|
+
if (update === null || typeof update !== "object") return update;
|
|
3848
|
+
const doc = update;
|
|
3849
|
+
for (const operator of ["$set", "$setOnInsert"]) {
|
|
3850
|
+
const op = doc[operator];
|
|
3851
|
+
if (op !== null && typeof op === "object" && !Array.isArray(op)) {
|
|
3852
|
+
const opDoc = op;
|
|
3853
|
+
for (const key of Object.keys(opDoc)) {
|
|
3854
|
+
if (opDoc[key] === void 0) delete opDoc[key];
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
for (const key of Object.keys(doc)) {
|
|
3859
|
+
if (doc[key] === void 0) delete doc[key];
|
|
3860
|
+
}
|
|
3861
|
+
return update;
|
|
3862
|
+
};
|
|
3863
|
+
|
|
3238
3864
|
// src/service/drift.ts
|
|
3239
3865
|
var checkDocsDrift = (options, collection, op, docs) => {
|
|
3240
3866
|
if (options.drift === "off") return;
|
|
@@ -3288,6 +3914,8 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3288
3914
|
};
|
|
3289
3915
|
const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
|
|
3290
3916
|
const stampCreate = (collection, doc) => {
|
|
3917
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3918
|
+
stripUndefinedKeys(doc);
|
|
3291
3919
|
const ts = timestampsFor(collection);
|
|
3292
3920
|
if (!ts) return;
|
|
3293
3921
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -3295,6 +3923,7 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3295
3923
|
if (ts.updatedAt && doc[ts.updatedAt] === void 0) doc[ts.updatedAt] = now;
|
|
3296
3924
|
};
|
|
3297
3925
|
const stampUpdate = (collection, update) => {
|
|
3926
|
+
stripUndefinedFromUpdate(update);
|
|
3298
3927
|
const formatted = formatUpdateFilter(update);
|
|
3299
3928
|
const ts = timestampsFor(collection);
|
|
3300
3929
|
if (ts?.updatedAt) {
|
|
@@ -3304,11 +3933,13 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3304
3933
|
return formatted;
|
|
3305
3934
|
};
|
|
3306
3935
|
const stampReplace = (collection, doc) => {
|
|
3936
|
+
applySchemaDefaults(opts.getSchema?.(String(collection)), doc);
|
|
3937
|
+
stripUndefinedKeys(doc);
|
|
3307
3938
|
const ts = timestampsFor(collection);
|
|
3308
3939
|
if (!ts?.updatedAt) return doc;
|
|
3309
3940
|
return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
|
|
3310
3941
|
};
|
|
3311
|
-
const read = async (collection, opName, filter, options, execute, shape = "none") => {
|
|
3942
|
+
const read = async (collection, opName, filter, options, execute, shape = "none", keyExtra) => {
|
|
3312
3943
|
const physical = resolve(String(collection));
|
|
3313
3944
|
const resolved = resolveQueryOptions(options);
|
|
3314
3945
|
const noSession = !resolved.sdk.session;
|
|
@@ -3338,10 +3969,22 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3338
3969
|
delayMs: resolved.retryDelayMs
|
|
3339
3970
|
})
|
|
3340
3971
|
);
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3972
|
+
const {
|
|
3973
|
+
maxAttempts: _ma,
|
|
3974
|
+
retryDelayMs: _rd,
|
|
3975
|
+
dedupe: _dd,
|
|
3976
|
+
cache: _ca,
|
|
3977
|
+
retryWrites: _rw,
|
|
3978
|
+
...resultOptions
|
|
3979
|
+
} = options ?? {};
|
|
3980
|
+
let payloadHash;
|
|
3981
|
+
const getPayloadHash = () => {
|
|
3982
|
+
if (payloadHash === void 0) {
|
|
3983
|
+
payloadHash = stableHash([opName, keyExtra ?? "", filter, resultOptions]);
|
|
3984
|
+
}
|
|
3985
|
+
return payloadHash;
|
|
3986
|
+
};
|
|
3987
|
+
const key = useCache ? opts.cache.key(cacheCollectionKey(client.databaseName, physical), getPayloadHash()) : void 0;
|
|
3345
3988
|
const fetchAndCheck = () => executeWrapped().then((value) => {
|
|
3346
3989
|
checkDrift(value);
|
|
3347
3990
|
return value;
|
|
@@ -3360,10 +4003,11 @@ var createCrudContext = (client, dbLabel, logger, opts) => {
|
|
|
3360
4003
|
return fetchAndCheck();
|
|
3361
4004
|
};
|
|
3362
4005
|
if (shouldDedupe) {
|
|
3363
|
-
const
|
|
3364
|
-
|
|
4006
|
+
const colKey = cacheCollectionKey(client.databaseName, physical);
|
|
4007
|
+
const gen = opts.cache !== void 0 ? opts.cache.versionOf(colKey) : "";
|
|
4008
|
+
return await opts.inFlight.run(`${colKey}|${gen}|${getPayloadHash()}`, runOnce);
|
|
3365
4009
|
}
|
|
3366
|
-
return runOnce();
|
|
4010
|
+
return await runOnce();
|
|
3367
4011
|
};
|
|
3368
4012
|
const normalizeFindOptions = (options) => {
|
|
3369
4013
|
if (!options) return {};
|
|
@@ -3422,7 +4066,10 @@ var makeReadOps = (ctx) => {
|
|
|
3422
4066
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3423
4067
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3424
4068
|
};
|
|
3425
|
-
return coll(collection).findOne(
|
|
4069
|
+
return await coll(collection).findOne(
|
|
4070
|
+
toDriverFilter(f),
|
|
4071
|
+
opts2
|
|
4072
|
+
);
|
|
3426
4073
|
},
|
|
3427
4074
|
"one"
|
|
3428
4075
|
);
|
|
@@ -3516,7 +4163,7 @@ var makeReadOps = (ctx) => {
|
|
|
3516
4163
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3517
4164
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3518
4165
|
};
|
|
3519
|
-
return coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
4166
|
+
return await coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
|
|
3520
4167
|
},
|
|
3521
4168
|
"none"
|
|
3522
4169
|
);
|
|
@@ -3541,21 +4188,24 @@ var makeReadOps = (ctx) => {
|
|
|
3541
4188
|
opts2
|
|
3542
4189
|
);
|
|
3543
4190
|
},
|
|
3544
|
-
"none"
|
|
4191
|
+
"none",
|
|
4192
|
+
String(field)
|
|
3545
4193
|
);
|
|
3546
4194
|
};
|
|
3547
|
-
const estimatedDocumentCount = async (collection, options) =>
|
|
4195
|
+
const estimatedDocumentCount = async (collection, options) => read(
|
|
3548
4196
|
collection,
|
|
3549
4197
|
"mongo.estimatedDocumentCount",
|
|
4198
|
+
{},
|
|
4199
|
+
options,
|
|
3550
4200
|
async (r) => {
|
|
3551
4201
|
const opts2 = {
|
|
3552
4202
|
...r.driverOpts,
|
|
3553
4203
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
3554
4204
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {}
|
|
3555
4205
|
};
|
|
3556
|
-
return coll(collection).estimatedDocumentCount(opts2);
|
|
4206
|
+
return await coll(collection).estimatedDocumentCount(opts2);
|
|
3557
4207
|
},
|
|
3558
|
-
|
|
4208
|
+
"none"
|
|
3559
4209
|
);
|
|
3560
4210
|
return {
|
|
3561
4211
|
getOne,
|
|
@@ -3592,7 +4242,7 @@ var makeWatchOps = (ctx) => {
|
|
|
3592
4242
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
3593
4243
|
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
3594
4244
|
};
|
|
3595
|
-
return execute(opts);
|
|
4245
|
+
return await execute(opts);
|
|
3596
4246
|
},
|
|
3597
4247
|
opName === "mongo.getOne" ? "one" : opName === "mongo.findMany" ? "many" : "none"
|
|
3598
4248
|
)
|
|
@@ -3609,6 +4259,20 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3609
4259
|
const { opts } = ctx;
|
|
3610
4260
|
const { coll, toDriverFilter, writeRun, invalidate, stampCreate, stampUpdate, stampReplace } = ctx;
|
|
3611
4261
|
const { getOne } = readOps;
|
|
4262
|
+
const safeAfterHooks = async (collection, name, hookCtx) => {
|
|
4263
|
+
try {
|
|
4264
|
+
await runHooks(opts.hooks, collection, name, hookCtx);
|
|
4265
|
+
} catch (err) {
|
|
4266
|
+
ctx.logger.error?.(
|
|
4267
|
+
{
|
|
4268
|
+
collection,
|
|
4269
|
+
hook: name,
|
|
4270
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4271
|
+
},
|
|
4272
|
+
`post-commit ${name} hook failed; DB write is already committed`
|
|
4273
|
+
);
|
|
4274
|
+
}
|
|
4275
|
+
};
|
|
3612
4276
|
const insertOne = async (collection, doc, options) => {
|
|
3613
4277
|
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
3614
4278
|
collection: String(collection),
|
|
@@ -3623,28 +4287,31 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3623
4287
|
...r.driverOpts,
|
|
3624
4288
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3625
4289
|
};
|
|
3626
|
-
return coll(collection).insertOne(
|
|
4290
|
+
return await coll(collection).insertOne(
|
|
4291
|
+
doc,
|
|
4292
|
+
insertOpts
|
|
4293
|
+
);
|
|
3627
4294
|
},
|
|
3628
4295
|
options
|
|
3629
4296
|
);
|
|
3630
4297
|
invalidate(collection);
|
|
3631
|
-
await
|
|
4298
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3632
4299
|
collection: String(collection),
|
|
3633
4300
|
doc
|
|
3634
4301
|
});
|
|
3635
4302
|
return result;
|
|
3636
4303
|
};
|
|
3637
4304
|
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
4305
|
if (docs.length > MAX_BATCH_OPS) {
|
|
3644
4306
|
throw new BadRequest(
|
|
3645
4307
|
`insertMany: too many docs (${docs.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
3646
4308
|
);
|
|
3647
4309
|
}
|
|
4310
|
+
await runHooks(opts.hooks, String(collection), "beforeCreate", {
|
|
4311
|
+
collection: String(collection),
|
|
4312
|
+
docs
|
|
4313
|
+
});
|
|
4314
|
+
for (const d of docs) stampCreate(collection, d);
|
|
3648
4315
|
const result = await writeRun(
|
|
3649
4316
|
collection,
|
|
3650
4317
|
"mongo.insertMany",
|
|
@@ -3661,7 +4328,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3661
4328
|
options
|
|
3662
4329
|
);
|
|
3663
4330
|
invalidate(collection);
|
|
3664
|
-
await
|
|
4331
|
+
await safeAfterHooks(String(collection), "afterCreate", {
|
|
3665
4332
|
collection: String(collection),
|
|
3666
4333
|
docs
|
|
3667
4334
|
});
|
|
@@ -3690,13 +4357,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3690
4357
|
options
|
|
3691
4358
|
);
|
|
3692
4359
|
invalidate(collection);
|
|
3693
|
-
await
|
|
4360
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
3694
4361
|
collection: String(collection),
|
|
3695
4362
|
filter
|
|
3696
4363
|
});
|
|
3697
4364
|
return result;
|
|
3698
4365
|
};
|
|
3699
4366
|
const updateMany = async (collection, filter, update, options) => {
|
|
4367
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4368
|
+
collection: String(collection),
|
|
4369
|
+
filter
|
|
4370
|
+
});
|
|
3700
4371
|
const effective = stampUpdate(collection, update);
|
|
3701
4372
|
const result = await writeRun(
|
|
3702
4373
|
collection,
|
|
@@ -3715,9 +4386,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3715
4386
|
options
|
|
3716
4387
|
);
|
|
3717
4388
|
invalidate(collection);
|
|
4389
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4390
|
+
collection: String(collection),
|
|
4391
|
+
filter
|
|
4392
|
+
});
|
|
3718
4393
|
return result;
|
|
3719
4394
|
};
|
|
3720
4395
|
const findOneAndUpdate = async (collection, filter, update, options) => {
|
|
4396
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4397
|
+
collection: String(collection),
|
|
4398
|
+
filter
|
|
4399
|
+
});
|
|
3721
4400
|
const effective = stampUpdate(collection, update);
|
|
3722
4401
|
const result = await writeRun(
|
|
3723
4402
|
collection,
|
|
@@ -3740,9 +4419,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3740
4419
|
options
|
|
3741
4420
|
);
|
|
3742
4421
|
invalidate(collection);
|
|
4422
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4423
|
+
collection: String(collection),
|
|
4424
|
+
filter
|
|
4425
|
+
});
|
|
3743
4426
|
return result;
|
|
3744
4427
|
};
|
|
3745
4428
|
const findOneAndReplace = async (collection, filter, replacement, options) => {
|
|
4429
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4430
|
+
collection: String(collection),
|
|
4431
|
+
filter
|
|
4432
|
+
});
|
|
3746
4433
|
const effective = stampReplace(collection, replacement);
|
|
3747
4434
|
const result = await writeRun(
|
|
3748
4435
|
collection,
|
|
@@ -3765,9 +4452,17 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3765
4452
|
options
|
|
3766
4453
|
);
|
|
3767
4454
|
invalidate(collection);
|
|
4455
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4456
|
+
collection: String(collection),
|
|
4457
|
+
filter
|
|
4458
|
+
});
|
|
3768
4459
|
return result;
|
|
3769
4460
|
};
|
|
3770
4461
|
const replaceOne = async (collection, filter, replacement, options) => {
|
|
4462
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4463
|
+
collection: String(collection),
|
|
4464
|
+
filter
|
|
4465
|
+
});
|
|
3771
4466
|
const result = await writeRun(
|
|
3772
4467
|
collection,
|
|
3773
4468
|
"mongo.replaceOne",
|
|
@@ -3785,6 +4480,10 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3785
4480
|
options
|
|
3786
4481
|
);
|
|
3787
4482
|
invalidate(collection);
|
|
4483
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4484
|
+
collection: String(collection),
|
|
4485
|
+
filter
|
|
4486
|
+
});
|
|
3788
4487
|
return result;
|
|
3789
4488
|
};
|
|
3790
4489
|
const deleteOne = async (collection, filter, options) => {
|
|
@@ -3800,18 +4499,22 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3800
4499
|
...r.driverOpts,
|
|
3801
4500
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3802
4501
|
};
|
|
3803
|
-
return coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
4502
|
+
return await coll(collection).deleteOne(toDriverFilter(filter), delOpts);
|
|
3804
4503
|
},
|
|
3805
4504
|
options
|
|
3806
4505
|
);
|
|
3807
4506
|
invalidate(collection);
|
|
3808
|
-
await
|
|
4507
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
3809
4508
|
collection: String(collection),
|
|
3810
4509
|
filter
|
|
3811
4510
|
});
|
|
3812
4511
|
return result;
|
|
3813
4512
|
};
|
|
3814
4513
|
const deleteMany = async (collection, filter, options) => {
|
|
4514
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4515
|
+
collection: String(collection),
|
|
4516
|
+
filter
|
|
4517
|
+
});
|
|
3815
4518
|
const result = await writeRun(
|
|
3816
4519
|
collection,
|
|
3817
4520
|
"mongo.deleteMany",
|
|
@@ -3820,14 +4523,22 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3820
4523
|
...r.driverOpts,
|
|
3821
4524
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3822
4525
|
};
|
|
3823
|
-
return coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
4526
|
+
return await coll(collection).deleteMany(toDriverFilter(filter), delOpts);
|
|
3824
4527
|
},
|
|
3825
4528
|
options
|
|
3826
4529
|
);
|
|
3827
4530
|
invalidate(collection);
|
|
4531
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4532
|
+
collection: String(collection),
|
|
4533
|
+
filter
|
|
4534
|
+
});
|
|
3828
4535
|
return result;
|
|
3829
4536
|
};
|
|
3830
4537
|
const findOneAndDelete = async (collection, filter, options) => {
|
|
4538
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4539
|
+
collection: String(collection),
|
|
4540
|
+
filter
|
|
4541
|
+
});
|
|
3831
4542
|
const result = await writeRun(
|
|
3832
4543
|
collection,
|
|
3833
4544
|
"mongo.findOneAndDelete",
|
|
@@ -3846,10 +4557,21 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3846
4557
|
options
|
|
3847
4558
|
);
|
|
3848
4559
|
invalidate(collection);
|
|
4560
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4561
|
+
collection: String(collection),
|
|
4562
|
+
filter
|
|
4563
|
+
});
|
|
3849
4564
|
return result;
|
|
3850
4565
|
};
|
|
3851
4566
|
const softDeleteOne = async (collection, filter, options) => {
|
|
4567
|
+
await runHooks(opts.hooks, String(collection), "beforeDelete", {
|
|
4568
|
+
collection: String(collection),
|
|
4569
|
+
filter
|
|
4570
|
+
});
|
|
3852
4571
|
const active = mergeMongoActiveFilter(true, filter);
|
|
4572
|
+
const effective = stampUpdate(collection, {
|
|
4573
|
+
$set: { deletedAt: /* @__PURE__ */ new Date() }
|
|
4574
|
+
});
|
|
3853
4575
|
const result = await writeRun(
|
|
3854
4576
|
collection,
|
|
3855
4577
|
"mongo.softDeleteOne",
|
|
@@ -3860,16 +4582,24 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3860
4582
|
};
|
|
3861
4583
|
return coll(collection).updateOne(
|
|
3862
4584
|
active,
|
|
3863
|
-
|
|
4585
|
+
effective,
|
|
3864
4586
|
updateOpts
|
|
3865
4587
|
);
|
|
3866
4588
|
},
|
|
3867
4589
|
options
|
|
3868
4590
|
);
|
|
3869
4591
|
invalidate(collection);
|
|
4592
|
+
await safeAfterHooks(String(collection), "afterDelete", {
|
|
4593
|
+
collection: String(collection),
|
|
4594
|
+
filter
|
|
4595
|
+
});
|
|
3870
4596
|
return result;
|
|
3871
4597
|
};
|
|
3872
4598
|
const upsert = async (collection, filter, update, options) => {
|
|
4599
|
+
await runHooks(opts.hooks, String(collection), "beforeUpdate", {
|
|
4600
|
+
collection: String(collection),
|
|
4601
|
+
filter
|
|
4602
|
+
});
|
|
3873
4603
|
const formatted = stampUpdate(collection, update);
|
|
3874
4604
|
const result = await writeRun(
|
|
3875
4605
|
collection,
|
|
@@ -3880,14 +4610,23 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3880
4610
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
3881
4611
|
upsert: true
|
|
3882
4612
|
};
|
|
3883
|
-
return coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
4613
|
+
return await coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
|
|
3884
4614
|
},
|
|
3885
4615
|
options
|
|
3886
4616
|
);
|
|
3887
4617
|
invalidate(collection);
|
|
4618
|
+
await safeAfterHooks(String(collection), "afterUpdate", {
|
|
4619
|
+
collection: String(collection),
|
|
4620
|
+
filter
|
|
4621
|
+
});
|
|
3888
4622
|
return result;
|
|
3889
4623
|
};
|
|
3890
4624
|
const bulkUpsert = async (collection, operations, options) => {
|
|
4625
|
+
if (operations.length > MAX_BATCH_OPS) {
|
|
4626
|
+
throw new BadRequest(
|
|
4627
|
+
`bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
3891
4630
|
const writes = operations.map((op) => ({
|
|
3892
4631
|
updateOne: {
|
|
3893
4632
|
filter: op.filter,
|
|
@@ -3895,11 +4634,6 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3895
4634
|
upsert: op.upsert ?? true
|
|
3896
4635
|
}
|
|
3897
4636
|
}));
|
|
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
4637
|
const result = await writeRun(
|
|
3904
4638
|
collection,
|
|
3905
4639
|
"mongo.bulkUpsert",
|
|
@@ -3908,7 +4642,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3908
4642
|
...r.driverOpts,
|
|
3909
4643
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3910
4644
|
};
|
|
3911
|
-
return coll(collection).bulkWrite(writes, bulkOpts);
|
|
4645
|
+
return await coll(collection).bulkWrite(writes, bulkOpts);
|
|
3912
4646
|
},
|
|
3913
4647
|
options
|
|
3914
4648
|
);
|
|
@@ -3929,7 +4663,7 @@ var makeWriteOps = (ctx, readOps) => {
|
|
|
3929
4663
|
...r.driverOpts,
|
|
3930
4664
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {}
|
|
3931
4665
|
};
|
|
3932
|
-
return coll(collection).bulkWrite(operations, bulkOpts);
|
|
4666
|
+
return await coll(collection).bulkWrite(operations, bulkOpts);
|
|
3933
4667
|
},
|
|
3934
4668
|
options
|
|
3935
4669
|
);
|
|
@@ -4001,14 +4735,29 @@ var tag = (value) => {
|
|
|
4001
4735
|
var untag = (tagged) => {
|
|
4002
4736
|
switch (tagged.t) {
|
|
4003
4737
|
case "oid":
|
|
4738
|
+
if (typeof tagged.v !== "string") {
|
|
4739
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 oid value must be a string");
|
|
4740
|
+
}
|
|
4004
4741
|
return new ObjectId(tagged.v);
|
|
4005
4742
|
case "date":
|
|
4743
|
+
if (typeof tagged.v !== "string") {
|
|
4744
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 date value must be a string");
|
|
4745
|
+
}
|
|
4006
4746
|
return new Date(tagged.v);
|
|
4007
4747
|
case "string":
|
|
4748
|
+
if (typeof tagged.v !== "string") {
|
|
4749
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 string value expected");
|
|
4750
|
+
}
|
|
4008
4751
|
return tagged.v;
|
|
4009
4752
|
case "number":
|
|
4753
|
+
if (typeof tagged.v !== "number") {
|
|
4754
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 number value expected");
|
|
4755
|
+
}
|
|
4010
4756
|
return tagged.v;
|
|
4011
4757
|
case "boolean":
|
|
4758
|
+
if (typeof tagged.v !== "boolean") {
|
|
4759
|
+
throw new BadRequest("paginateCursor: invalid cursor \u2014 boolean value expected");
|
|
4760
|
+
}
|
|
4012
4761
|
return tagged.v;
|
|
4013
4762
|
case "null":
|
|
4014
4763
|
return null;
|
|
@@ -4094,7 +4843,8 @@ var makePaginateCursor = (ctx) => {
|
|
|
4094
4843
|
...r.driverOpts,
|
|
4095
4844
|
...r.sdk.session !== void 0 ? { session: r.sdk.session } : {},
|
|
4096
4845
|
...r.sdk.maxTimeMS !== void 0 ? { maxTimeMS: r.sdk.maxTimeMS } : {},
|
|
4097
|
-
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {}
|
|
4846
|
+
...r.sdk.hint !== void 0 ? { hint: r.sdk.hint } : {},
|
|
4847
|
+
...r.sdk.batchSize !== void 0 ? { batchSize: r.sdk.batchSize } : {}
|
|
4098
4848
|
};
|
|
4099
4849
|
return client.collection(resolve(String(collection))).find(filter, findOpts).sort(effectiveSort).limit(limit + 1).toArray();
|
|
4100
4850
|
},
|
|
@@ -4114,10 +4864,16 @@ var makePaginateCursor = (ctx) => {
|
|
|
4114
4864
|
page
|
|
4115
4865
|
);
|
|
4116
4866
|
const boundary = backwards ? rows[limit] : page[page.length - 1];
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
values
|
|
4120
|
-
|
|
4867
|
+
let nextCursor = null;
|
|
4868
|
+
if (hasMore && boundary !== void 0) {
|
|
4869
|
+
const values = sortKeys.map(([f]) => boundary[f]);
|
|
4870
|
+
if (values.some((v) => v === void 0)) {
|
|
4871
|
+
throw new BadRequest(
|
|
4872
|
+
"paginateCursor: boundary document is missing a sort field \u2014 every sort key must exist on every document for keyset pagination"
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4875
|
+
nextCursor = encodeCursor({ sort: config.sort, values });
|
|
4876
|
+
}
|
|
4121
4877
|
return { data: page, nextCursor, hasMore };
|
|
4122
4878
|
};
|
|
4123
4879
|
return { paginateCursor };
|
|
@@ -4132,13 +4888,17 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4132
4888
|
...config.page !== void 0 ? { page: config.page } : {},
|
|
4133
4889
|
...config.limit !== void 0 ? { limit: config.limit } : {}
|
|
4134
4890
|
},
|
|
4135
|
-
config.maxLimit ?? DEFAULT_MAX_LIMIT
|
|
4891
|
+
config.maxLimit ?? DEFAULT_MAX_LIMIT,
|
|
4892
|
+
// Same default as `findMany`/`paginateCursor` — the two pagination
|
|
4893
|
+
// APIs used to disagree silently (10 vs 100).
|
|
4894
|
+
{ limit: DEFAULT_FIND_LIMIT }
|
|
4136
4895
|
);
|
|
4137
4896
|
const dataStages = [
|
|
4138
4897
|
...config.prePipeline ?? [],
|
|
4139
4898
|
...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
|
|
4140
4899
|
{ $skip: normalized.offset },
|
|
4141
4900
|
{ $limit: normalized.limit },
|
|
4901
|
+
...config.projection !== void 0 ? [{ $project: config.projection }] : [],
|
|
4142
4902
|
...config.postPipeline ?? []
|
|
4143
4903
|
];
|
|
4144
4904
|
const pipeline = [
|
|
@@ -4167,17 +4927,19 @@ var makePaginateFlexible = (ctx) => {
|
|
|
4167
4927
|
const row = rows[0];
|
|
4168
4928
|
const data = row?.data ?? [];
|
|
4169
4929
|
const totalCount = row?.totalCount?.[0]?.count ?? 0;
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4930
|
+
if (config.projection === void 0) {
|
|
4931
|
+
checkDocsDrift(
|
|
4932
|
+
{
|
|
4933
|
+
logger,
|
|
4934
|
+
db: dbLabel,
|
|
4935
|
+
drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
|
|
4936
|
+
...opts.getSchema !== void 0 ? { getSchema: opts.getSchema } : {}
|
|
4937
|
+
},
|
|
4938
|
+
String(collection),
|
|
4939
|
+
"mongo.paginateFlexible",
|
|
4940
|
+
data
|
|
4941
|
+
);
|
|
4942
|
+
}
|
|
4181
4943
|
return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
|
|
4182
4944
|
}
|
|
4183
4945
|
});
|
|
@@ -4205,9 +4967,12 @@ var makePaginationOps = (client, dbLabel, logger, opts) => {
|
|
|
4205
4967
|
};
|
|
4206
4968
|
|
|
4207
4969
|
// src/service/schema-ops.ts
|
|
4208
|
-
var
|
|
4209
|
-
|
|
4210
|
-
)
|
|
4970
|
+
var optionSignature = (o) => JSON.stringify([
|
|
4971
|
+
Boolean(o?.unique),
|
|
4972
|
+
Boolean(o?.sparse),
|
|
4973
|
+
typeof o?.expireAfterSeconds === "number" ? o.expireAfterSeconds : null
|
|
4974
|
+
]);
|
|
4975
|
+
var indexKeyOf = (key, options) => `${JSON.stringify(Object.keys(key).map((k) => [k, key[k]]))}|${optionSignature(options)}`;
|
|
4211
4976
|
var makeSchemaOps = (client, _logger, opts) => {
|
|
4212
4977
|
const createSchema = async (collection, createCollectionOptions) => {
|
|
4213
4978
|
const physical = opts.resolveCollectionName(collection);
|
|
@@ -4215,8 +4980,10 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4215
4980
|
const validator = def?.schema ? toMongoValidator(def.schema) : void 0;
|
|
4216
4981
|
try {
|
|
4217
4982
|
await client.createCollection(physical, {
|
|
4218
|
-
|
|
4219
|
-
|
|
4983
|
+
// Caller options FIRST, derived validator LAST — a stale `validator`
|
|
4984
|
+
// key in the options must not clobber the schema-derived one.
|
|
4985
|
+
...createCollectionOptions ?? {},
|
|
4986
|
+
...validator ? { validator } : {}
|
|
4220
4987
|
});
|
|
4221
4988
|
} catch (err) {
|
|
4222
4989
|
if (err?.code === 48) {
|
|
@@ -4261,9 +5028,11 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4261
5028
|
const handle = client.collection(physical);
|
|
4262
5029
|
const existing = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4263
5030
|
const existingByKey = new Map(
|
|
4264
|
-
existing.map((i) => [indexKeyOf(i.key), i.name])
|
|
5031
|
+
existing.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5032
|
+
);
|
|
5033
|
+
const declaredByKey = new Map(
|
|
5034
|
+
declared.map((d) => [indexKeyOf(d.key, d.options), d])
|
|
4265
5035
|
);
|
|
4266
|
-
const declaredByKey = new Map(declared.map((d) => [indexKeyOf(d.key), d]));
|
|
4267
5036
|
const created = [];
|
|
4268
5037
|
const dropped = [];
|
|
4269
5038
|
for (const [key] of declaredByKey) {
|
|
@@ -4275,7 +5044,9 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4275
5044
|
}
|
|
4276
5045
|
}
|
|
4277
5046
|
const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
|
|
4278
|
-
const afterByKey = new Map(
|
|
5047
|
+
const afterByKey = new Map(
|
|
5048
|
+
after.map((i) => [indexKeyOf(i.key, i), i.name])
|
|
5049
|
+
);
|
|
4279
5050
|
for (const [key, spec] of declaredByKey) {
|
|
4280
5051
|
if (!existingByKey.has(key)) {
|
|
4281
5052
|
created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
|
|
@@ -4294,16 +5065,22 @@ var makeSchemaOps = (client, _logger, opts) => {
|
|
|
4294
5065
|
|
|
4295
5066
|
// src/service/transaction.ts
|
|
4296
5067
|
var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
5068
|
+
let warnedUnsupported = false;
|
|
5069
|
+
let journalIndexReady = false;
|
|
5070
|
+
const MIGRATE_LEASE_MS = 12e4;
|
|
4297
5071
|
const transaction = async (fn, options) => {
|
|
4298
5072
|
const session = client.startSession();
|
|
4299
5073
|
try {
|
|
4300
5074
|
return await session.withTransaction(async () => fn(session), options);
|
|
4301
5075
|
} catch (err) {
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
5076
|
+
if (isTransactionUnsupportedError(err)) {
|
|
5077
|
+
if (!warnedUnsupported) {
|
|
5078
|
+
warnedUnsupported = true;
|
|
5079
|
+
logger.warn?.(
|
|
5080
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
5081
|
+
"transactions unsupported on this deployment, falling back to non-transactional"
|
|
5082
|
+
);
|
|
5083
|
+
}
|
|
4307
5084
|
return fn(null);
|
|
4308
5085
|
}
|
|
4309
5086
|
if (opts.wrapMongoErrors) {
|
|
@@ -4315,34 +5092,105 @@ var makeTransactionOps = (client, handle, logger, _config, opts) => {
|
|
|
4315
5092
|
}
|
|
4316
5093
|
};
|
|
4317
5094
|
const migrate = async (name, fn, filePath) => {
|
|
4318
|
-
const journal = handle.collection(
|
|
4319
|
-
|
|
4320
|
-
|
|
5095
|
+
const journal = handle.collection(opts.migrationCollectionPhysical);
|
|
5096
|
+
if (!journalIndexReady) {
|
|
5097
|
+
try {
|
|
5098
|
+
await journal.createIndex({ name: 1 }, { unique: true });
|
|
5099
|
+
} catch {
|
|
5100
|
+
}
|
|
5101
|
+
journalIndexReady = true;
|
|
5102
|
+
}
|
|
5103
|
+
const isDupKeyError = (err) => {
|
|
5104
|
+
const code = err?.code;
|
|
5105
|
+
if (code === 11e3 || code === 11001) return true;
|
|
5106
|
+
return /E11000 duplicate key/.test(String(err?.message ?? ""));
|
|
5107
|
+
};
|
|
4321
5108
|
await transaction(async (session) => {
|
|
4322
5109
|
const sessionOpts = session ? { session } : {};
|
|
4323
|
-
const
|
|
4324
|
-
|
|
5110
|
+
const now = /* @__PURE__ */ new Date();
|
|
5111
|
+
const expiresAt = new Date(Date.now() + MIGRATE_LEASE_MS);
|
|
5112
|
+
const stole = await journal.findOneAndUpdate(
|
|
4325
5113
|
{
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
5114
|
+
name,
|
|
5115
|
+
status: { $exists: true, $nin: ["applied"] },
|
|
5116
|
+
$or: [
|
|
5117
|
+
{ expiresAt: { $lt: now } },
|
|
5118
|
+
{
|
|
5119
|
+
expiresAt: { $exists: false },
|
|
5120
|
+
appliedAt: { $lt: new Date(now.getTime() - MIGRATE_LEASE_MS) }
|
|
5121
|
+
}
|
|
5122
|
+
]
|
|
5123
|
+
},
|
|
5124
|
+
{
|
|
5125
|
+
$set: {
|
|
5126
|
+
status: "running",
|
|
5127
|
+
appliedAt: now,
|
|
5128
|
+
expiresAt,
|
|
4329
5129
|
...filePath ? { file: filePath } : {}
|
|
4330
5130
|
}
|
|
4331
5131
|
},
|
|
4332
|
-
{
|
|
5132
|
+
{ returnDocument: "before", ...sessionOpts }
|
|
4333
5133
|
);
|
|
4334
|
-
if (
|
|
4335
|
-
|
|
5134
|
+
if (stole !== null) {
|
|
5135
|
+
await runClaimed(session);
|
|
4336
5136
|
return;
|
|
4337
5137
|
}
|
|
5138
|
+
try {
|
|
5139
|
+
await journal.insertOne(
|
|
5140
|
+
{
|
|
5141
|
+
name,
|
|
5142
|
+
appliedAt: now,
|
|
5143
|
+
status: "running",
|
|
5144
|
+
expiresAt,
|
|
5145
|
+
...filePath ? { file: filePath } : {}
|
|
5146
|
+
},
|
|
5147
|
+
sessionOpts
|
|
5148
|
+
);
|
|
5149
|
+
} catch (err) {
|
|
5150
|
+
if (!isDupKeyError(err)) throw err;
|
|
5151
|
+
const existing = await journal.findOne(
|
|
5152
|
+
{ name },
|
|
5153
|
+
{ projection: { status: 1 }, ...sessionOpts }
|
|
5154
|
+
);
|
|
5155
|
+
const status = existing?.status;
|
|
5156
|
+
if (status === void 0 || status === "applied") {
|
|
5157
|
+
logger.info({ name }, "migration already applied, skipping");
|
|
5158
|
+
} else {
|
|
5159
|
+
logger.warn({ name }, "migration claim held by another runner, skipping");
|
|
5160
|
+
}
|
|
5161
|
+
return;
|
|
5162
|
+
}
|
|
5163
|
+
await runClaimed(session);
|
|
5164
|
+
});
|
|
5165
|
+
async function runClaimed(session) {
|
|
5166
|
+
const sessionOpts = session ? { session } : {};
|
|
5167
|
+
const heartbeat = session === null ? setInterval(() => {
|
|
5168
|
+
void journal.updateOne(
|
|
5169
|
+
{ name, status: "running" },
|
|
5170
|
+
{ $set: { expiresAt: new Date(Date.now() + MIGRATE_LEASE_MS) } }
|
|
5171
|
+
).catch(() => {
|
|
5172
|
+
});
|
|
5173
|
+
}, MIGRATE_LEASE_MS / 2) : void 0;
|
|
5174
|
+
heartbeat?.unref?.();
|
|
4338
5175
|
try {
|
|
4339
5176
|
await fn(session);
|
|
5177
|
+
const res = await journal.updateOne(
|
|
5178
|
+
{ name, status: "running" },
|
|
5179
|
+
{ $set: { status: "applied" } },
|
|
5180
|
+
sessionOpts
|
|
5181
|
+
);
|
|
5182
|
+
if (res?.matchedCount === 0) {
|
|
5183
|
+
await journal.insertOne({ name, appliedAt: /* @__PURE__ */ new Date(), status: "applied" }, sessionOpts).catch(() => {
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
4340
5186
|
} catch (err) {
|
|
4341
5187
|
await journal.deleteOne({ name }, sessionOpts).catch(() => {
|
|
4342
5188
|
});
|
|
4343
5189
|
throw err;
|
|
5190
|
+
} finally {
|
|
5191
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
4344
5192
|
}
|
|
4345
|
-
}
|
|
5193
|
+
}
|
|
4346
5194
|
};
|
|
4347
5195
|
return { transaction, migrate };
|
|
4348
5196
|
};
|
|
@@ -4487,11 +5335,27 @@ var createMongoService = (dbClients, configOverrides = {}) => {
|
|
|
4487
5335
|
};
|
|
4488
5336
|
|
|
4489
5337
|
// src/shared/merge-filters.ts
|
|
5338
|
+
var isOperatorDoc = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4490
5339
|
var mergeMongoFilters = (...parts) => {
|
|
4491
5340
|
const out = {};
|
|
4492
5341
|
for (const part of parts) {
|
|
4493
5342
|
if (!part) continue;
|
|
4494
|
-
Object.
|
|
5343
|
+
for (const [key, value] of Object.entries(part)) {
|
|
5344
|
+
if (!(key in out)) {
|
|
5345
|
+
out[key] = value;
|
|
5346
|
+
continue;
|
|
5347
|
+
}
|
|
5348
|
+
if (key === "$and" || key === "$or") {
|
|
5349
|
+
const prev2 = out[key];
|
|
5350
|
+
out[key] = [
|
|
5351
|
+
...Array.isArray(prev2) ? prev2 : [prev2],
|
|
5352
|
+
...Array.isArray(value) ? value : [value]
|
|
5353
|
+
];
|
|
5354
|
+
continue;
|
|
5355
|
+
}
|
|
5356
|
+
const prev = out[key];
|
|
5357
|
+
out[key] = isOperatorDoc(prev) && isOperatorDoc(value) ? { ...prev, ...value } : value;
|
|
5358
|
+
}
|
|
4495
5359
|
}
|
|
4496
5360
|
return out;
|
|
4497
5361
|
};
|
|
@@ -4570,6 +5434,6 @@ var createCachedAsyncFactory = (factory, options = {}) => {
|
|
|
4570
5434
|
};
|
|
4571
5435
|
};
|
|
4572
5436
|
|
|
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 };
|
|
5437
|
+
export { AppError, BadRequest, CacheInvalidator, DEFAULT_FIND_LIMIT, DEFAULT_MAX_LIMIT, DataLoader, DomainError, ERROR_HTTP_STATUS, HOOK_NAMES, HotCache, InFlight, InfraError, LRU, MAX_BATCH_OPS, QueryBuilder, QueryCache, TRANSIENT_MONGO_ERROR_CODES, belongsTo, buildKeysetFilter, buildMongoActiveFilter, buildPaginationResult, cacheCollectionKey, canonicalKey, createCachedAsyncFactory, createCachedFactory, createConsoleLogger, createHotCache, createMongoCapabilitiesStore, createMongoMigrationRunner, createMongoService, createMongoToolkit, createNoopLogger, createRepository, createResolveCollectionName, decodeCursor, defineCollection, defineCollections, encodeCursor, hasMany, hashString, httpStatusForError, isAppError, isDomainError, isInfraError, isMappedMongoError, isMongoDuplicateKeyError, isMongoTransientError, isTransactionUnsupportedError, manyToMany, mapMongoDriverError, mergeMongoActiveFilter, mergeMongoFilters, normalizeMongoUrl, normalizePageLimit, optional, probeMongoCapabilities, runHooks, s, serializeError, sleep, sleepJittered, stableHash, stableStringify, stripDocumentId, stripPrimaryKey, toMongoSchema, toMongoValidator, validateDoc, withDefault, withGracefulMongoTransaction, withRetry, withTimeout, withTransaction };
|
|
4574
5438
|
//# sourceMappingURL=index.js.map
|
|
4575
5439
|
//# sourceMappingURL=index.js.map
|