@ignex/ninox 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,4370 @@
1
+ import { ObjectId, MongoClient, Decimal128 } from 'mongodb';
2
+ import { writeFile, mkdir, readdir } from 'fs/promises';
3
+ import { basename, join } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+
6
+ // src/utils/timeout.ts
7
+ var withTimeout = (promise, timeoutMs, label) => {
8
+ if (timeoutMs <= 0) return promise;
9
+ let timer;
10
+ const timeout = new Promise((_, reject) => {
11
+ timer = setTimeout(() => {
12
+ reject(new Error(`Operation timed out after ${timeoutMs}ms: ${label}`));
13
+ }, timeoutMs);
14
+ });
15
+ return Promise.race([promise, timeout]).finally(() => {
16
+ if (timer !== void 0) clearTimeout(timer);
17
+ });
18
+ };
19
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
+ var sleepJittered = (ms, jitterMs = 1e3) => sleep(ms + Math.floor(Math.random() * Math.min(ms, jitterMs)));
21
+
22
+ // src/capabilities.ts
23
+ var createMongoCapabilitiesStore = () => {
24
+ let state = { probed: false, transactionsSupported: false };
25
+ return {
26
+ get: () => state,
27
+ set: (capabilities) => {
28
+ state = capabilities;
29
+ },
30
+ reset: () => {
31
+ state = { probed: false, transactionsSupported: false };
32
+ }
33
+ };
34
+ };
35
+ var readMongoTransactionsEnvOverride = () => {
36
+ const raw = process.env.MONGO_USE_TRANSACTIONS;
37
+ if (raw === void 0) return void 0;
38
+ return ["1", "true", "yes"].includes(raw.toLowerCase());
39
+ };
40
+ var probeMongoCapabilities = async (client) => {
41
+ try {
42
+ const db = client && "startSession" in client ? client.db() : client;
43
+ if (!db) return { probed: false, transactionsSupported: false };
44
+ const hello = await withTimeout(
45
+ db.admin().command({ hello: 1 }),
46
+ 5e3,
47
+ "probeMongoCapabilities: hello"
48
+ );
49
+ const isReplicaSet = typeof hello.setName === "string" && hello.setName.length > 0;
50
+ const isMongos = hello.msg === "isdbgrid";
51
+ return { probed: true, transactionsSupported: isReplicaSet || isMongos };
52
+ } catch {
53
+ return { probed: false, transactionsSupported: false };
54
+ }
55
+ };
56
+ var mongoTransactionsEnabled = (store, options = {}) => {
57
+ const env = options.envOverride ?? readMongoTransactionsEnvOverride();
58
+ if (env !== void 0) return env;
59
+ const state = store.get();
60
+ if (state.probed) return state.transactionsSupported;
61
+ if (options.urlHint) return /replicaSet=/.test(options.urlHint);
62
+ return false;
63
+ };
64
+
65
+ // src/errors/classes.ts
66
+ var ERROR_HTTP_STATUS = {
67
+ BAD_REQUEST: 400,
68
+ NOT_FOUND: 404,
69
+ DUPLICATE_KEY: 409,
70
+ VERSION_CONFLICT: 409,
71
+ COLLECTION_EXISTS: 409,
72
+ SCHEMA_DRIFT: 409,
73
+ VALIDATION_FAILED: 422,
74
+ MONGO_TIMEOUT: 504
75
+ };
76
+ var AppError = class extends Error {
77
+ code;
78
+ statusCode;
79
+ extra;
80
+ constructor(code, message, statusCode = 500, extra) {
81
+ super(message);
82
+ this.name = "AppError";
83
+ this.code = code;
84
+ this.statusCode = statusCode;
85
+ this.extra = extra;
86
+ }
87
+ /**
88
+ * Stable, client-safe serialization for framework error middleware: a
89
+ * framework can forward `JSON.stringify(error)` (or `error.toJSON()`) straight
90
+ * to the client — no parsing of server internals. `stack` is dropped; the
91
+ * shape is always `{ name, code, message, statusCode, ...(extra) }`.
92
+ */
93
+ toJSON() {
94
+ return {
95
+ name: this.name,
96
+ code: this.code,
97
+ message: this.message,
98
+ statusCode: this.statusCode,
99
+ ...this.extra !== void 0 ? { extra: this.extra } : {}
100
+ };
101
+ }
102
+ };
103
+ var DomainError = class extends AppError {
104
+ constructor(code, message, extra) {
105
+ super(code, message, ERROR_HTTP_STATUS[code] ?? 400, extra);
106
+ this.name = "DomainError";
107
+ }
108
+ };
109
+ var InfraError = class extends AppError {
110
+ constructor(code, message, extra) {
111
+ super(code, message, ERROR_HTTP_STATUS[code] ?? 500, extra);
112
+ this.name = "InfraError";
113
+ }
114
+ };
115
+ var BadRequest = class extends DomainError {
116
+ constructor(message, extra) {
117
+ super("BAD_REQUEST", message, extra);
118
+ this.name = "BadRequest";
119
+ }
120
+ };
121
+ var isAppError = (err) => err instanceof AppError;
122
+ var isDomainError = (err) => err instanceof DomainError;
123
+ var isInfraError = (err) => err instanceof InfraError;
124
+
125
+ // src/errors/transient.ts
126
+ var TRANSIENT_MONGO_ERROR_CODES = Object.freeze(
127
+ /* @__PURE__ */ new Set([
128
+ 6,
129
+ // HostUnreachable
130
+ 7,
131
+ // HostNotFound
132
+ 89,
133
+ // NetworkTimeout
134
+ 91,
135
+ // ShutdownInProgress
136
+ 189,
137
+ // PrimarySteppedDown
138
+ 9001,
139
+ // SocketException
140
+ 10107,
141
+ // NotWritablePrimary
142
+ 11600,
143
+ // InterruptedAtShutdown
144
+ 11602,
145
+ // InterruptedDueToReplStateChange
146
+ 13435,
147
+ // NotPrimaryNoSecondaryOk
148
+ 13436
149
+ // NotPrimaryOrSecondary
150
+ ])
151
+ );
152
+ var TRANSIENT_NAME = /MongoNetworkError|PoolClearedError|MongoWriteConcernError|MongoNotPrimaryError|MongoNodeIsRecoveringError|MongoNetworkTimeoutError/;
153
+ var isMongoTransientError = (error) => {
154
+ if (!error || typeof error !== "object") return false;
155
+ const e = error;
156
+ if (typeof e.name === "string" && TRANSIENT_NAME.test(e.name)) return true;
157
+ if (typeof e.code === "number" && TRANSIENT_MONGO_ERROR_CODES.has(e.code)) return true;
158
+ return false;
159
+ };
160
+ var isMongoDuplicateKeyError = (error) => {
161
+ if (!error || typeof error !== "object") return false;
162
+ const e = error;
163
+ if (e.code === 11e3) return true;
164
+ if (typeof e.message === "string" && /E11000|duplicate key/i.test(e.message)) return true;
165
+ return false;
166
+ };
167
+
168
+ // src/errors/driver-map.ts
169
+ var extractValidationPaths = (errInfo) => {
170
+ if (!errInfo || typeof errInfo !== "object") return [];
171
+ const info = errInfo;
172
+ const out = [];
173
+ for (const rule of info.details?.schemaRulesNotSatisfied ?? []) {
174
+ if (typeof rule.propertyName === "string") out.push(rule.propertyName);
175
+ for (const prop of rule.propertiesNotSatisfied ?? []) {
176
+ if (typeof prop.propertyName === "string") out.push(prop.propertyName);
177
+ }
178
+ for (const prop of rule.additionalProperties ?? []) {
179
+ if (typeof prop === "string") out.push(prop);
180
+ }
181
+ }
182
+ return [...new Set(out)];
183
+ };
184
+ var addValidationContext = (context, errInfo) => {
185
+ if (!errInfo || typeof errInfo !== "object") return;
186
+ const info = errInfo;
187
+ if (info.failingDocumentId !== void 0) context.documentId = info.failingDocumentId;
188
+ const fields = extractValidationPaths(errInfo);
189
+ if (fields.length > 0) context.fields = fields;
190
+ if (info.details !== void 0) context.details = info.details;
191
+ };
192
+ var isBulkWriteError = (error) => typeof error === "object" && error !== null && "writeErrors" in error && Array.isArray(error.writeErrors);
193
+ var mapMongoDriverError = (error, ctx = {}) => {
194
+ if (error instanceof AppError) return error;
195
+ const context = { ...ctx };
196
+ if (isMongoDuplicateKeyError(error)) {
197
+ const e = error;
198
+ if (e.keyPattern) context.keyPattern = e.keyPattern;
199
+ if (e.keyValue) context.keyValue = e.keyValue;
200
+ return new DomainError("DUPLICATE_KEY", "Duplicate key error", context);
201
+ }
202
+ if (isBulkWriteError(error)) {
203
+ const first = error.writeErrors[0];
204
+ if (first?.code === 11e3) {
205
+ if (first.keyPattern) context.keyPattern = first.keyPattern;
206
+ if (first.keyValue) context.keyValue = first.keyValue;
207
+ return new DomainError("DUPLICATE_KEY", first.errmsg ?? "Duplicate key error", context);
208
+ }
209
+ if (first?.code === 121) {
210
+ addValidationContext(context, first.errInfo);
211
+ return new DomainError("VALIDATION_FAILED", "Document failed schema validation", context);
212
+ }
213
+ return new InfraError("MONGO_QUERY_ERROR", "Bulk write failed", context);
214
+ }
215
+ const code = error.code;
216
+ switch (code) {
217
+ case 112:
218
+ return new DomainError("VERSION_CONFLICT", "Document version conflict", context);
219
+ case 50:
220
+ return new InfraError("MONGO_TIMEOUT", "MongoDB operation timed out", context);
221
+ case 121: {
222
+ addValidationContext(context, error.errInfo);
223
+ return new DomainError("VALIDATION_FAILED", "Document failed schema validation", context);
224
+ }
225
+ }
226
+ if (isMongoTransientError(error)) {
227
+ const e = error;
228
+ return new InfraError("MONGO_QUERY_ERROR", e.message ?? "Transient MongoDB error", context);
229
+ }
230
+ const msg = error.message ?? "MongoDB query error";
231
+ return new InfraError("MONGO_QUERY_ERROR", msg, context);
232
+ };
233
+ var isMappedMongoError = (error) => error instanceof AppError;
234
+
235
+ // src/errors/http-status.ts
236
+ var httpStatusForError = (err) => {
237
+ if (err instanceof AppError) return err.statusCode;
238
+ if (isMongoTransientError(err)) return 503;
239
+ return 500;
240
+ };
241
+ var serializeError = (err, ctx = {}) => {
242
+ const mapped = mapMongoDriverError(err, ctx);
243
+ if (mapped instanceof AppError) return mapped.toJSON();
244
+ if (mapped instanceof Error) {
245
+ return { name: mapped.name, code: "UNKNOWN", message: mapped.message, statusCode: 500 };
246
+ }
247
+ return { name: "UnknownError", code: "UNKNOWN", message: String(mapped), statusCode: 500 };
248
+ };
249
+ var cloneDeep = (value) => {
250
+ if (value instanceof Date) return new Date(value.getTime());
251
+ if (value instanceof ObjectId) return new ObjectId(value.toHexString());
252
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
253
+ if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
254
+ if (value !== null && typeof value === "object") {
255
+ const out = {};
256
+ for (const [k, v] of Object.entries(value)) {
257
+ out[k] = cloneDeep(v);
258
+ }
259
+ return out;
260
+ }
261
+ return value;
262
+ };
263
+
264
+ // src/utils/hash.ts
265
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
266
+ var toPrimitive = (v) => {
267
+ if (v === void 0 || v === null) return null;
268
+ if (typeof v === "bigint") return v.toString();
269
+ if (v instanceof Date) return `date:${v.getTime()}`;
270
+ if (typeof v === "object") {
271
+ const anyV = v;
272
+ if (typeof anyV.toHexString === "function") return `oid:${anyV.toHexString()}`;
273
+ if (anyV instanceof RegExp) return `re:${anyV.source}/${anyV.flags}`;
274
+ }
275
+ return v;
276
+ };
277
+ var stableStringify = (value) => {
278
+ const visit = (v) => {
279
+ if (Array.isArray(v)) return v.map(visit);
280
+ if (isPlainObject(v)) {
281
+ const out = {};
282
+ for (const key of Object.keys(v).sort()) out[key] = visit(v[key]);
283
+ return out;
284
+ }
285
+ return toPrimitive(v);
286
+ };
287
+ return JSON.stringify(visit(value));
288
+ };
289
+ var hashString = (input) => {
290
+ let hash = 5381;
291
+ for (let i = 0; i < input.length; i++) {
292
+ hash = (hash << 5) + hash + input.charCodeAt(i) | 0;
293
+ }
294
+ return (hash >>> 0).toString(36);
295
+ };
296
+ var stableHash = (value) => hashString(stableStringify(value));
297
+
298
+ // src/utils/lru.ts
299
+ var LRU = class {
300
+ max;
301
+ map = /* @__PURE__ */ new Map();
302
+ /** Number of entries evicted to make room for new ones (capacity evictions only). */
303
+ evictions = 0;
304
+ constructor(options) {
305
+ if (options.max <= 0) throw new Error("LRU max must be > 0");
306
+ this.max = options.max;
307
+ }
308
+ get size() {
309
+ return this.map.size;
310
+ }
311
+ get(key) {
312
+ const value = this.map.get(key);
313
+ if (value === void 0) return void 0;
314
+ this.map.delete(key);
315
+ this.map.set(key, value);
316
+ return value;
317
+ }
318
+ peek(key) {
319
+ return this.map.get(key);
320
+ }
321
+ has(key) {
322
+ return this.map.has(key);
323
+ }
324
+ set(key, value) {
325
+ if (this.map.has(key)) this.map.delete(key);
326
+ this.map.set(key, value);
327
+ if (this.map.size > this.max) {
328
+ const oldest = this.map.keys().next().value;
329
+ if (oldest !== void 0) {
330
+ this.map.delete(oldest);
331
+ this.evictions++;
332
+ }
333
+ }
334
+ return this;
335
+ }
336
+ delete(key) {
337
+ return this.map.delete(key);
338
+ }
339
+ clear() {
340
+ this.map.clear();
341
+ }
342
+ keys() {
343
+ return this.map.keys();
344
+ }
345
+ entries() {
346
+ return this.map.entries();
347
+ }
348
+ [Symbol.iterator]() {
349
+ return this.map.entries();
350
+ }
351
+ };
352
+
353
+ // src/cache/in-flight.ts
354
+ var InFlight = class {
355
+ pending = /* @__PURE__ */ new Map();
356
+ get size() {
357
+ return this.pending.size;
358
+ }
359
+ run(key, fn) {
360
+ const existing = this.pending.get(key);
361
+ if (existing) return existing;
362
+ const promise = fn().finally(() => {
363
+ this.pending.delete(key);
364
+ });
365
+ this.pending.set(key, promise);
366
+ return promise;
367
+ }
368
+ clear() {
369
+ this.pending.clear();
370
+ }
371
+ };
372
+ var estimateSize = (value) => {
373
+ if (value === null || value === void 0) return 0;
374
+ if (typeof value === "string") return value.length * 2;
375
+ if (typeof value === "number" || typeof value === "boolean") return 8;
376
+ if (value instanceof ObjectId) return 16;
377
+ if (value instanceof Date) return 8;
378
+ if (Array.isArray(value)) return value.reduce((sum, v) => sum + estimateSize(v), 0);
379
+ if (typeof value === "object") {
380
+ let sum = 0;
381
+ for (const [k, v] of Object.entries(value)) {
382
+ sum += k.length * 2 + estimateSize(v);
383
+ }
384
+ return sum;
385
+ }
386
+ return 0;
387
+ };
388
+
389
+ // src/cache/hot-cache/ticker.ts
390
+ var RefreshTicker = class {
391
+ constructor(host, tickIntervalMs) {
392
+ this.host = host;
393
+ this.intervalMs = tickIntervalMs;
394
+ }
395
+ timer;
396
+ intervalMs;
397
+ /** Whether the timer is currently running. */
398
+ get running() {
399
+ return this.timer !== void 0;
400
+ }
401
+ /** Start the interval (idempotent). The timer is `.unref()`'d so it never holds the process open. */
402
+ start() {
403
+ if (this.timer) return;
404
+ this.timer = setInterval(() => {
405
+ try {
406
+ this.tick();
407
+ } catch (err) {
408
+ this.host.logger.warn?.(
409
+ { error: err instanceof Error ? err.message : String(err) },
410
+ "hot cache tick failed"
411
+ );
412
+ }
413
+ }, this.intervalMs);
414
+ const t = this.timer;
415
+ t?.unref?.();
416
+ }
417
+ /** Stop the interval (idempotent). */
418
+ stop() {
419
+ if (this.timer) {
420
+ clearInterval(this.timer);
421
+ this.timer = void 0;
422
+ }
423
+ }
424
+ /** Background refresh pass: re-run loaders for due entries, swap fresh values. */
425
+ tick() {
426
+ const now = Date.now();
427
+ for (const q of this.host.queries().values()) {
428
+ if (q.refreshIntervalMs <= 0) continue;
429
+ for (const [key, entry] of [...q.lru.entries()]) {
430
+ if (entry.nextRefreshAt <= now) {
431
+ q.refreshes++;
432
+ void this.host.fetch(q, key, entry.args, q.gen).catch((err) => {
433
+ const current = q.lru.peek(key);
434
+ if (current) current.nextRefreshAt = now + q.refreshIntervalMs;
435
+ this.host.logger.warn?.(
436
+ {
437
+ error: err instanceof Error ? err.message : String(err),
438
+ query: q.name
439
+ },
440
+ "hot cache background refresh failed (stale value retained)"
441
+ );
442
+ });
443
+ }
444
+ }
445
+ }
446
+ }
447
+ /**
448
+ * In standalone mode the ticker only refreshes queries with an explicit
449
+ * `refreshIntervalMs`. A query with neither `refreshIntervalMs` nor `ttlMs`
450
+ * set is served until it is manually invalidated — an easy way to serve
451
+ * unbounded-stale data. Surface those queries once so the operator opts in
452
+ * deliberately.
453
+ */
454
+ warnUnboundedStaleness() {
455
+ const unbounded = [...this.host.queries().values()].filter(
456
+ (q) => q.refreshIntervalMs <= 0 && q.ttlMs <= 0
457
+ );
458
+ if (unbounded.length === 0) return;
459
+ this.host.logger.warn?.(
460
+ { queries: unbounded.map((q) => q.name) },
461
+ "hot cache: standalone mode \u2014 these queries have no refreshIntervalMs/ttlMs, so values are served until manually invalidated (unbounded staleness); set refreshIntervalMs or ttlMs to bound it"
462
+ );
463
+ }
464
+ };
465
+
466
+ // src/cache/hot-cache/types.ts
467
+ var WATCH_SEP = "\0";
468
+
469
+ // src/cache/hot-cache/watcher.ts
470
+ var WatchCoordinator = class {
471
+ constructor(host) {
472
+ this.host = host;
473
+ }
474
+ streams = /* @__PURE__ */ new Map();
475
+ failedStreams = /* @__PURE__ */ new Set();
476
+ stopped = true;
477
+ /** Number of currently open change streams. */
478
+ get size() {
479
+ return this.streams.size;
480
+ }
481
+ /** Mark the coordinator as running and open watchers for all watch refs (idempotent). */
482
+ start() {
483
+ this.stopped = false;
484
+ this.openWatchStreams();
485
+ }
486
+ /** Close every watcher and clear failure bookkeeping (idempotent). */
487
+ async stop() {
488
+ this.stopped = true;
489
+ const closes = [...this.streams.values()].map((stream) => {
490
+ try {
491
+ return Promise.resolve(stream.close()).catch(() => {
492
+ });
493
+ } catch {
494
+ return Promise.resolve();
495
+ }
496
+ });
497
+ this.streams.clear();
498
+ this.failedStreams.clear();
499
+ await Promise.allSettled(closes);
500
+ }
501
+ openWatchStreams() {
502
+ const seen = /* @__PURE__ */ new Set();
503
+ for (const q of this.host.queries().values()) {
504
+ for (const ref of q.config.watch ?? []) {
505
+ const key = `${ref.db.databaseName}${WATCH_SEP}${ref.collection}`;
506
+ if (seen.has(key) || this.streams.has(key)) continue;
507
+ seen.add(key);
508
+ void this.watchLoop(key, ref);
509
+ }
510
+ }
511
+ }
512
+ /**
513
+ * Long-lived change-stream consumer for one collection. Each data change
514
+ * invalidates the queries bound to it. On permanent "not supported" errors the
515
+ * watcher is dropped and, if every planned stream failed, the cache falls back
516
+ * to the standalone ticker. Transient errors retry with backoff.
517
+ */
518
+ async watchLoop(key, ref) {
519
+ let backoff = 0;
520
+ while (!this.stopped) {
521
+ try {
522
+ const stream = ref.db.collection(ref.collection).watch([]);
523
+ stream.on("error", () => {
524
+ });
525
+ this.streams.set(key, stream);
526
+ if (backoff > 0) {
527
+ this.host.logger.warn?.(
528
+ { collection: ref.collection },
529
+ "hot cache: change stream reconnected \u2014 invalidating collection to drop entries missed during the outage"
530
+ );
531
+ this.host.invalidateCollection(ref.collection);
532
+ }
533
+ const iterator = stream[Symbol.asyncIterator]();
534
+ for await (const _change of iterator) {
535
+ this.host.invalidateCollection(ref.collection);
536
+ }
537
+ } catch (err) {
538
+ const message = err instanceof Error ? err.message : String(err);
539
+ this.host.logger.warn?.(
540
+ { error: message, collection: ref.collection },
541
+ "hot cache change stream error"
542
+ );
543
+ const abandoned = this.streams.get(key);
544
+ this.streams.delete(key);
545
+ if (abandoned) {
546
+ try {
547
+ await abandoned.close();
548
+ } catch {
549
+ }
550
+ }
551
+ if (/only supported on replica sets|not supported|ChangeStream/i.test(message)) {
552
+ this.markStreamFailed(key);
553
+ return;
554
+ }
555
+ }
556
+ if (this.stopped) return;
557
+ backoff = Math.min(backoff + 1e3, 5e3);
558
+ await sleepJittered(backoff);
559
+ }
560
+ }
561
+ markStreamFailed(key) {
562
+ this.failedStreams.add(key);
563
+ if (!this.host.isReplica()) return;
564
+ const planned = this.plannedStreamKeys();
565
+ if (planned.size > 0 && [...planned].every((k) => this.failedStreams.has(k))) {
566
+ this.host.logger.warn?.({}, "hot cache: change streams unsupported, falling back to ticker");
567
+ this.host.fallbackToStandalone();
568
+ }
569
+ }
570
+ plannedStreamKeys() {
571
+ const set = /* @__PURE__ */ new Set();
572
+ for (const q of this.host.queries().values()) {
573
+ for (const ref of q.config.watch ?? []) {
574
+ set.add(`${ref.db.databaseName}${WATCH_SEP}${ref.collection}`);
575
+ }
576
+ }
577
+ return set;
578
+ }
579
+ };
580
+
581
+ // src/cache/hot-cache/index.ts
582
+ var HotCache = class {
583
+ options;
584
+ logger;
585
+ refreshEnabled;
586
+ queries = /* @__PURE__ */ new Map();
587
+ inflight = new InFlight();
588
+ watcher;
589
+ ticker;
590
+ _mode = "unknown";
591
+ started = false;
592
+ startAttempted = false;
593
+ stopped = false;
594
+ disposed = false;
595
+ startPromise;
596
+ constructor(options = {}) {
597
+ this.options = options;
598
+ this.refreshEnabled = options.autoRefresh !== false;
599
+ this.logger = options.logger ?? { debug() {
600
+ }, info() {
601
+ }, warn() {
602
+ }, error() {
603
+ } };
604
+ this.ticker = new RefreshTicker(
605
+ {
606
+ queries: () => this.queries,
607
+ fetch: (q, key, args, gen) => this.fetch(q, key, args, gen),
608
+ logger: this.logger
609
+ },
610
+ options.tickIntervalMs ?? 1e3
611
+ );
612
+ this.watcher = new WatchCoordinator({
613
+ queries: () => this.queries,
614
+ invalidateCollection: (collection) => this.invalidateCollection(collection),
615
+ isReplica: () => this._mode === "replica",
616
+ fallbackToStandalone: () => {
617
+ this._mode = "standalone";
618
+ if (this.refreshEnabled) this.ticker.start();
619
+ },
620
+ logger: this.logger
621
+ });
622
+ }
623
+ /** Number of cached entries across all registered queries. */
624
+ get size() {
625
+ let total = 0;
626
+ for (const q of this.queries.values()) total += q.lru.size;
627
+ return total;
628
+ }
629
+ /** Resolved mode: `unknown` until `start()` (or the first `get`) probes. */
630
+ get mode() {
631
+ return this._mode;
632
+ }
633
+ /** Whether the standalone background-refresh ticker is enabled (`autoRefresh`). */
634
+ get autoRefresh() {
635
+ return this.refreshEnabled;
636
+ }
637
+ /** Whether a query with this name has been registered. */
638
+ has(name) {
639
+ return this.queries.has(name);
640
+ }
641
+ /**
642
+ * Opt a query into the hot cache. Throws `BadRequest` on a duplicate name.
643
+ * Returns a fully typed accessor — `.get(...args)` (intellisense on the loader
644
+ * parameters) and `.invalidate()` / `.invalidate(...args)` for manual
645
+ * invalidation (all entries or a single parameter set).
646
+ */
647
+ register(name, config) {
648
+ if (this.queries.has(name)) {
649
+ throw new BadRequest(`hot cache: query '${name}' is already registered`);
650
+ }
651
+ const query = {
652
+ name,
653
+ config,
654
+ lru: new LRU({
655
+ max: config.maxSize ?? this.options.defaultMaxSize ?? 500
656
+ }),
657
+ ttlMs: config.ttlMs ?? this.options.defaultTtlMs ?? 0,
658
+ refreshIntervalMs: config.refreshIntervalMs ?? 0,
659
+ clone: config.clone ?? false,
660
+ maxValueBytes: config.maxValueBytes,
661
+ gen: 0,
662
+ hits: 0,
663
+ misses: 0,
664
+ refreshes: 0,
665
+ loadErrors: 0,
666
+ sizeSkips: 0,
667
+ warnedSize: false
668
+ };
669
+ this.queries.set(name, query);
670
+ if (this._mode === "replica" && !this.stopped) {
671
+ this.watcher.start();
672
+ }
673
+ return {
674
+ get: (...args) => this.get(name, ...args),
675
+ invalidate: (...args) => {
676
+ if (args.length === 0) this.invalidate(name);
677
+ else this.invalidateParams(name, ...args);
678
+ }
679
+ };
680
+ }
681
+ /**
682
+ * Read-through read by dynamic name lookup. Call parameters are hashed into
683
+ * the cache key (`keyOf` override wins). Throws `BadRequest` for an unknown
684
+ * query name. For fully typed parameters/results prefer the accessor returned
685
+ * by `register()` — this dynamic form returns `Promise<unknown>`.
686
+ */
687
+ async get(name, ...args) {
688
+ this.ensureStarted();
689
+ const q = this.queries.get(name);
690
+ if (!q) throw new BadRequest(`hot cache: unknown query '${name}'`);
691
+ const key = q.config.keyOf ? q.config.keyOf(...args) : stableHash(args);
692
+ const entry = q.lru.get(key);
693
+ if (entry && !(q.ttlMs > 0 && entry.expiresAt <= Date.now())) {
694
+ q.hits++;
695
+ return q.clone ? cloneDeep(entry.value) : entry.value;
696
+ }
697
+ if (entry && q.ttlMs > 0 && entry.expiresAt <= Date.now()) {
698
+ q.lru.delete(key);
699
+ }
700
+ q.misses++;
701
+ return this.fetch(q, key, args, q.gen);
702
+ }
703
+ /** Drop every cached entry for a registered query. */
704
+ invalidate(name) {
705
+ const q = this.queries.get(name);
706
+ if (!q) return;
707
+ q.lru.clear();
708
+ q.gen++;
709
+ }
710
+ /**
711
+ * Drop only the cached entry for a specific parameter set. The key is computed
712
+ * exactly like `get` (`keyOf` override ?? `stableHash` of args), so the same
713
+ * parameters invalidate the same entry.
714
+ */
715
+ invalidateParams(name, ...args) {
716
+ const q = this.queries.get(name);
717
+ if (!q) return;
718
+ const key = q.config.keyOf ? q.config.keyOf(...args) : stableHash(args);
719
+ q.lru.delete(key);
720
+ q.gen++;
721
+ }
722
+ /**
723
+ * Invalidate every query bound to a physical collection (via `watch`). Can be
724
+ * called manually to wire ORM write hooks in standalone mode.
725
+ */
726
+ invalidateCollection(collection) {
727
+ for (const q of this.queries.values()) {
728
+ if (q.config.watch?.some((ref) => ref.collection === collection)) {
729
+ q.lru.clear();
730
+ q.gen++;
731
+ }
732
+ }
733
+ }
734
+ /** Drop every cached entry for every query. */
735
+ clear() {
736
+ for (const q of this.queries.values()) {
737
+ q.lru.clear();
738
+ q.gen++;
739
+ }
740
+ }
741
+ /**
742
+ * Runtime observability snapshot: total queries/entries plus per-query
743
+ * counters (`hits`, `misses`, background `refreshes`, `loadErrors`, and
744
+ * `sizeSkips` for entries dropped by `maxValueBytes`). Useful for dashboards
745
+ * and cache-effectiveness checks.
746
+ */
747
+ stats() {
748
+ const perQuery = {};
749
+ for (const [name, q] of this.queries) {
750
+ perQuery[name] = {
751
+ size: q.lru.size,
752
+ hits: q.hits,
753
+ misses: q.misses,
754
+ refreshes: q.refreshes,
755
+ loadErrors: q.loadErrors,
756
+ sizeSkips: q.sizeSkips,
757
+ evictions: q.lru.evictions
758
+ };
759
+ }
760
+ return { mode: this._mode, queries: this.queries.size, entries: this.size, perQuery };
761
+ }
762
+ /**
763
+ * Probe replica support once, then start change-stream watchers (replica) or
764
+ * the global refresh ticker (standalone). Idempotent — resolves the mode.
765
+ */
766
+ async start() {
767
+ if (this.disposed) return this._mode;
768
+ if (this.started) return this._mode;
769
+ if (!this.startPromise) {
770
+ this.startPromise = this.runStart();
771
+ }
772
+ return this.startPromise;
773
+ }
774
+ /**
775
+ * Stop the ticker, close every change stream, and **dispose** the instance.
776
+ * Terminal: a later `get()` serves cached values read-through but never
777
+ * re-probes or re-opens background resources.
778
+ */
779
+ async stop() {
780
+ this.disposed = true;
781
+ this.stopped = true;
782
+ this.ticker.stop();
783
+ await this.watcher.stop();
784
+ this.inflight.clear();
785
+ for (const q of this.queries.values()) q.gen++;
786
+ this.started = false;
787
+ this.startAttempted = false;
788
+ this.startPromise = void 0;
789
+ this._mode = "unknown";
790
+ }
791
+ /* ----------------------------- internals ----------------------------- */
792
+ async runStart() {
793
+ this.stopped = false;
794
+ const supported = this.options.mode === "replica" ? true : this.options.mode === "standalone" ? false : await (this.options.probe ?? this.defaultProbe)();
795
+ if (this.disposed) {
796
+ this.started = false;
797
+ return this._mode;
798
+ }
799
+ if (supported) {
800
+ this._mode = "replica";
801
+ this.watcher.start();
802
+ } else {
803
+ this._mode = "standalone";
804
+ if (this.refreshEnabled) {
805
+ this.ticker.start();
806
+ this.ticker.warnUnboundedStaleness();
807
+ }
808
+ }
809
+ this.started = true;
810
+ return this._mode;
811
+ }
812
+ ensureStarted() {
813
+ if (this.disposed || this.started || this.startAttempted) return;
814
+ this.startAttempted = true;
815
+ void this.start().catch((err) => {
816
+ this.logger.warn?.(
817
+ { error: err instanceof Error ? err.message : String(err) },
818
+ "hot cache start failed; reads continue without auto-refresh"
819
+ );
820
+ });
821
+ }
822
+ defaultProbe = async () => {
823
+ for (const q of this.queries.values()) {
824
+ for (const ref of q.config.watch ?? []) {
825
+ const caps = await probeMongoCapabilities(ref.db);
826
+ if (!caps.probed) {
827
+ this.logger.warn?.(
828
+ { db: ref.db.databaseName },
829
+ "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"
830
+ );
831
+ return false;
832
+ }
833
+ return caps.transactionsSupported;
834
+ }
835
+ }
836
+ return false;
837
+ };
838
+ fetch(q, key, args, gen) {
839
+ const dedupeKey = `${q.name}${WATCH_SEP}${key}`;
840
+ return this.inflight.run(dedupeKey, async () => {
841
+ try {
842
+ const value = await q.config.loader(...args);
843
+ if (q.gen === gen) {
844
+ if (q.maxValueBytes !== void 0 && estimateSize(value) > q.maxValueBytes) {
845
+ q.sizeSkips++;
846
+ if (!q.warnedSize) {
847
+ q.warnedSize = true;
848
+ this.logger.warn?.(
849
+ { query: q.name, bytes: estimateSize(value), max: q.maxValueBytes },
850
+ "hot cache: value exceeds maxValueBytes \u2014 returned but not cached"
851
+ );
852
+ }
853
+ } else {
854
+ this.setEntry(q, key, args, value);
855
+ }
856
+ }
857
+ return value;
858
+ } catch (err) {
859
+ q.loadErrors++;
860
+ throw err;
861
+ }
862
+ });
863
+ }
864
+ setEntry(q, key, args, value) {
865
+ const now = Date.now();
866
+ q.lru.set(key, {
867
+ value,
868
+ args,
869
+ expiresAt: q.ttlMs > 0 ? now + q.ttlMs : Infinity,
870
+ nextRefreshAt: q.refreshIntervalMs > 0 ? now + q.refreshIntervalMs : Infinity
871
+ });
872
+ }
873
+ };
874
+ var createHotCache = (options) => new HotCache(options);
875
+
876
+ // src/cache/query-cache.ts
877
+ var SEP = "\0";
878
+ var NS_SEP = "";
879
+ var cacheCollectionKey = (dbName, physical) => `${dbName}${NS_SEP}${physical}`;
880
+ var QueryCache = class {
881
+ lru;
882
+ index = /* @__PURE__ */ new Map();
883
+ ttlMs;
884
+ clone;
885
+ hits = 0;
886
+ misses = 0;
887
+ sets = 0;
888
+ deletes = 0;
889
+ invalidateEvents = 0;
890
+ clearEvents = 0;
891
+ constructor(options = {}) {
892
+ this.lru = new LRU({ max: options.maxSize ?? 500 });
893
+ this.ttlMs = options.ttlMs ?? 0;
894
+ this.clone = options.clone ?? false;
895
+ }
896
+ get size() {
897
+ return this.lru.size;
898
+ }
899
+ /** Cache-health snapshot: sizes plus lifetime hit/miss/eviction counters. */
900
+ stats() {
901
+ return {
902
+ size: this.lru.size,
903
+ maxSize: this.lru.max,
904
+ hits: this.hits,
905
+ misses: this.misses,
906
+ sets: this.sets,
907
+ deletes: this.deletes,
908
+ invalidateEvents: this.invalidateEvents,
909
+ clearEvents: this.clearEvents,
910
+ evictions: this.lru.evictions
911
+ };
912
+ }
913
+ /** Compose a cache key for a collection + filter/options hash. */
914
+ key(collection, filterHash) {
915
+ return `${collection}${SEP}${filterHash}`;
916
+ }
917
+ get(key) {
918
+ const entry = this.lru.get(key);
919
+ if (!entry) {
920
+ this.misses++;
921
+ return void 0;
922
+ }
923
+ if (this.ttlMs > 0 && Date.now() > entry.expiresAt) {
924
+ this.lru.delete(key);
925
+ this.misses++;
926
+ return void 0;
927
+ }
928
+ this.hits++;
929
+ return this.clone ? cloneDeep(entry.value) : entry.value;
930
+ }
931
+ set(key, value, ttlMs) {
932
+ this.sets++;
933
+ const ttl = ttlMs ?? this.ttlMs;
934
+ const expiresAt = ttl > 0 ? Date.now() + ttl : Infinity;
935
+ this.lru.set(key, { value, expiresAt });
936
+ const sep = key.indexOf(SEP);
937
+ const collection = sep === -1 ? key : key.slice(0, sep);
938
+ let keys = this.index.get(collection);
939
+ if (!keys) {
940
+ keys = /* @__PURE__ */ new Set();
941
+ this.index.set(collection, keys);
942
+ }
943
+ keys.add(key);
944
+ }
945
+ delete(key) {
946
+ this.deletes++;
947
+ this.lru.delete(key);
948
+ }
949
+ /** Drop every cached entry belonging to a physical collection. */
950
+ invalidateByCollection(collection) {
951
+ const keys = this.index.get(collection);
952
+ if (!keys) return;
953
+ this.invalidateEvents++;
954
+ for (const key of keys) this.lru.delete(key);
955
+ this.index.delete(collection);
956
+ }
957
+ clear() {
958
+ this.clearEvents++;
959
+ this.lru.clear();
960
+ this.index.clear();
961
+ }
962
+ };
963
+
964
+ // src/graceful-transaction.ts
965
+ var withGracefulMongoTransaction = async (runner, fn, options = {}) => {
966
+ const enabled = options.capabilities === void 0 || mongoTransactionsEnabled(options.capabilities, { urlHint: options.urlHint });
967
+ if (!enabled) return fn(null);
968
+ try {
969
+ return await runner.transaction(fn);
970
+ } catch (err) {
971
+ const message = err instanceof Error ? err.message : "";
972
+ if (/Transaction numbers are only allowed|transactions are not supported/i.test(message)) {
973
+ options.logger?.warn?.({ error: message }, "transactions unsupported, falling back");
974
+ return fn(null);
975
+ }
976
+ if (options.wrapMongoErrors !== false) {
977
+ throw mapMongoDriverError(err, { op: "transaction" });
978
+ }
979
+ throw err;
980
+ }
981
+ };
982
+
983
+ // src/hooks/hooks.ts
984
+ var HOOK_NAMES = [
985
+ "beforeCreate",
986
+ "afterCreate",
987
+ "beforeUpdate",
988
+ "afterUpdate",
989
+ "beforeDelete",
990
+ "afterDelete",
991
+ "afterRead"
992
+ ];
993
+ var hasHook = (registry, collection, name) => registry?.[collection]?.[name] !== void 0;
994
+ var runHooks = async (registry, collection, name, ctx) => {
995
+ const hooks = registry?.[collection];
996
+ const hook = hooks?.[name];
997
+ if (!hook) return;
998
+ await hook(ctx);
999
+ };
1000
+
1001
+ // src/loader/dataloader.ts
1002
+ var DataLoader = class {
1003
+ batchFn;
1004
+ maxBatchSize;
1005
+ maxBatchDelayMs;
1006
+ cacheEnabled;
1007
+ keyOf;
1008
+ cache;
1009
+ queue = [];
1010
+ scheduled = false;
1011
+ flushing = false;
1012
+ constructor(options) {
1013
+ this.batchFn = options.batch;
1014
+ this.maxBatchSize = options.maxBatchSize ?? 1e3;
1015
+ this.maxBatchDelayMs = options.maxBatchDelayMs ?? 0;
1016
+ this.cacheEnabled = options.cache ?? true;
1017
+ this.keyOf = options.keyOf ?? ((key) => `${typeof key}:${String(key)}`);
1018
+ this.cache = options.cacheMap ?? /* @__PURE__ */ new Map();
1019
+ }
1020
+ /** Load one key. Concurrent loads are batched; cached keys resolve instantly. */
1021
+ load(key) {
1022
+ const canonicalKey2 = this.keyOf(key);
1023
+ if (this.cacheEnabled) {
1024
+ const cached = this.cache.get(canonicalKey2);
1025
+ if (cached) return cached;
1026
+ }
1027
+ let resolve;
1028
+ let reject;
1029
+ const promise = new Promise((res, rej) => {
1030
+ resolve = res;
1031
+ reject = rej;
1032
+ });
1033
+ this.queue.push({ key, canonicalKey: canonicalKey2, promise, resolve, reject });
1034
+ if (this.cacheEnabled) this.cache.set(canonicalKey2, promise);
1035
+ this.scheduleFlush();
1036
+ return promise;
1037
+ }
1038
+ /** Load many keys (each resolves independently). */
1039
+ loadMany(keys) {
1040
+ return Promise.all(keys.map((key) => this.load(key)));
1041
+ }
1042
+ clear(key) {
1043
+ this.cache.delete(this.keyOf(key));
1044
+ return this;
1045
+ }
1046
+ clearAll() {
1047
+ this.cache.clear();
1048
+ return this;
1049
+ }
1050
+ /** Pre-populate the cache without hitting the batch function. */
1051
+ prime(key, value) {
1052
+ if (this.cacheEnabled) this.cache.set(this.keyOf(key), Promise.resolve(value));
1053
+ return this;
1054
+ }
1055
+ get pendingCount() {
1056
+ return this.queue.length;
1057
+ }
1058
+ scheduleFlush() {
1059
+ if (this.scheduled) return;
1060
+ this.scheduled = true;
1061
+ if (this.maxBatchDelayMs > 0) {
1062
+ setTimeout(() => {
1063
+ void this.flush();
1064
+ }, this.maxBatchDelayMs);
1065
+ } else {
1066
+ queueMicrotask(() => {
1067
+ void this.flush();
1068
+ });
1069
+ }
1070
+ }
1071
+ async flush() {
1072
+ if (this.flushing) return;
1073
+ if (this.queue.length === 0) {
1074
+ this.scheduled = false;
1075
+ return;
1076
+ }
1077
+ const items = this.queue.splice(0, this.maxBatchSize);
1078
+ this.flushing = true;
1079
+ try {
1080
+ const resultMap = await this.batchFn(items.map((item) => item.key));
1081
+ for (const item of items) {
1082
+ item.resolve(resultMap.get(item.key));
1083
+ }
1084
+ } catch (error) {
1085
+ for (const item of items) {
1086
+ if (this.cacheEnabled && this.cache.get(item.canonicalKey) === item.promise) {
1087
+ this.cache.delete(item.canonicalKey);
1088
+ }
1089
+ item.reject(error);
1090
+ }
1091
+ } finally {
1092
+ this.flushing = false;
1093
+ this.scheduled = false;
1094
+ if (this.queue.length > 0) {
1095
+ this.scheduled = true;
1096
+ queueMicrotask(() => {
1097
+ void this.flush();
1098
+ });
1099
+ }
1100
+ }
1101
+ }
1102
+ };
1103
+ var canonicalKey = (value) => {
1104
+ if (value === void 0 || value === null) return "null";
1105
+ if (typeof value === "object") {
1106
+ const anyValue = value;
1107
+ if (typeof anyValue.toHexString === "function") return `oid:${anyValue.toHexString()}`;
1108
+ if (value instanceof Date) return `date:${value.getTime()}`;
1109
+ }
1110
+ return `${typeof value}:${String(value)}`;
1111
+ };
1112
+
1113
+ // src/migrations/types.ts
1114
+ var MIGRATION_FILE = /^(\d+)_.+\.(ts|js|mjs)$/;
1115
+ var JOURNAL_COLLECTION = "_migrations";
1116
+
1117
+ // src/migrations/files.ts
1118
+ var migrationNameOf = (file) => basename(file).replace(/\.(ts|js|mjs)$/, "");
1119
+ var listMigrationFiles = async (dir) => {
1120
+ const files = await readdir(dir);
1121
+ return files.filter((file) => MIGRATION_FILE.test(file)).sort();
1122
+ };
1123
+ var loadMigration = async (dir, file) => {
1124
+ const mod = await import(pathToFileURL(join(dir, file)).href);
1125
+ return { name: migrationNameOf(file), ...mod };
1126
+ };
1127
+ var ensureMigrationDir = async (dir) => {
1128
+ await mkdir(dir, { recursive: true });
1129
+ };
1130
+ var nextMigrationNumber = async (dir) => {
1131
+ const files = await readdir(dir);
1132
+ return files.reduce((max, file) => {
1133
+ const match = MIGRATION_FILE.exec(file);
1134
+ return match ? Math.max(max, Number(match[1])) : max;
1135
+ }, 0) + 1;
1136
+ };
1137
+
1138
+ // src/migrations/journal.ts
1139
+ var createMigrationJournal = (manager) => {
1140
+ const appliedNames = async () => {
1141
+ const rows = await manager.findMany(JOURNAL_COLLECTION);
1142
+ return new Set(
1143
+ rows.filter((row) => row.status === void 0 || row.status === "applied").map((row) => row.name)
1144
+ );
1145
+ };
1146
+ const cleanupStale = async () => {
1147
+ await manager.deleteMany(JOURNAL_COLLECTION, {
1148
+ status: "running",
1149
+ appliedAt: { $lt: new Date(Date.now() - 6e4) }
1150
+ }).catch(() => {
1151
+ });
1152
+ };
1153
+ const claim = async (name, file) => {
1154
+ const claimed = await manager.findOneAndUpdate(
1155
+ JOURNAL_COLLECTION,
1156
+ { name },
1157
+ {
1158
+ $setOnInsert: {
1159
+ name,
1160
+ appliedAt: /* @__PURE__ */ new Date(),
1161
+ status: "running",
1162
+ file
1163
+ }
1164
+ },
1165
+ { upsert: true, returnDocument: "before" }
1166
+ );
1167
+ return claimed === null;
1168
+ };
1169
+ const markApplied = async (name) => {
1170
+ await manager.updateOne(JOURNAL_COLLECTION, { name, status: "running" }, { $set: { status: "applied" } }).catch(async () => {
1171
+ await manager.insertOne(JOURNAL_COLLECTION, {
1172
+ name,
1173
+ appliedAt: /* @__PURE__ */ new Date(),
1174
+ status: "applied"
1175
+ }).catch(() => {
1176
+ });
1177
+ });
1178
+ };
1179
+ const removeRunning = async (name) => {
1180
+ await manager.deleteOne(JOURNAL_COLLECTION, { name, status: "running" }).catch(() => {
1181
+ });
1182
+ };
1183
+ const unrecord = async (name) => {
1184
+ await manager.deleteOne(JOURNAL_COLLECTION, { name });
1185
+ };
1186
+ return { appliedNames, cleanupStale, claim, markApplied, removeRunning, unrecord };
1187
+ };
1188
+
1189
+ // src/migrations/index.ts
1190
+ var createMongoMigrationRunner = (service, options = {}) => {
1191
+ const migrationDir = options.migrationDir ?? service.config?.migrationDir ?? "./migrations";
1192
+ const logger = options.logger ?? service.logger;
1193
+ const manager = () => {
1194
+ const key = Object.keys(service.db)[0];
1195
+ const db = key ? service.db[key] : void 0;
1196
+ if (!db) throw new Error("No database client connected \u2014 call service.makeConnections() first");
1197
+ return db;
1198
+ };
1199
+ const up = async () => {
1200
+ await ensureMigrationDir(migrationDir);
1201
+ const journal = createMigrationJournal(manager());
1202
+ await journal.cleanupStale();
1203
+ const files = await listMigrationFiles(migrationDir);
1204
+ const applied = await journal.appliedNames();
1205
+ const pending = files.filter((file) => !applied.has(migrationNameOf(file)));
1206
+ for (const file of pending) {
1207
+ const migration = await loadMigration(migrationDir, file);
1208
+ logger.info({ name: migration.name }, "running migration up");
1209
+ if (!await journal.claim(migration.name, basename(file))) {
1210
+ logger.info({ name: migration.name }, "migration already applied, skipping");
1211
+ continue;
1212
+ }
1213
+ try {
1214
+ await migration.up({ service, logger, name: migration.name });
1215
+ await journal.markApplied(migration.name);
1216
+ } catch (err) {
1217
+ await journal.removeRunning(migration.name);
1218
+ throw err;
1219
+ }
1220
+ logger.info({ name: migration.name }, "migration applied");
1221
+ }
1222
+ };
1223
+ const down = async (targetName) => {
1224
+ const files = await listMigrationFiles(migrationDir);
1225
+ const journal = createMigrationJournal(manager());
1226
+ const applied = await journal.appliedNames();
1227
+ const appliedFiles = files.filter((file) => applied.has(migrationNameOf(file))).reverse();
1228
+ for (const file of appliedFiles) {
1229
+ const migration = await loadMigration(migrationDir, file);
1230
+ logger.info({ name: migration.name }, "running migration down");
1231
+ await migration.down({ service, logger, name: migration.name });
1232
+ await journal.unrecord(migration.name);
1233
+ logger.info({ name: migration.name }, "migration rolled back");
1234
+ if (targetName && migration.name === targetName) break;
1235
+ }
1236
+ };
1237
+ const status = async () => {
1238
+ const files = await listMigrationFiles(migrationDir);
1239
+ const applied = await createMigrationJournal(manager()).appliedNames();
1240
+ const all = files.map(migrationNameOf);
1241
+ return {
1242
+ applied: all.filter((name) => applied.has(name)),
1243
+ pending: all.filter((name) => !applied.has(name))
1244
+ };
1245
+ };
1246
+ const create = async (name) => {
1247
+ await ensureMigrationDir(migrationDir);
1248
+ const next = await nextMigrationNumber(migrationDir);
1249
+ const padded = String(next).padStart(3, "0");
1250
+ const fileName = `${padded}_${name}.ts`;
1251
+ const filePath = join(migrationDir, fileName);
1252
+ const template = `import type { MigrationContext } from '@ignex/ninox';
1253
+
1254
+ export const up = async (ctx: MigrationContext): Promise<void> => {
1255
+ // const users = ctx.service.db.primaryClient;
1256
+ // await users.createSchema('users');
1257
+ };
1258
+
1259
+ export const down = async (ctx: MigrationContext): Promise<void> => {
1260
+ // const users = ctx.service.db.primaryClient;
1261
+ // await users.client.dropCollection('users');
1262
+ };
1263
+ `;
1264
+ await writeFile(filePath, template, "utf8");
1265
+ logger.info({ name, file: fileName }, "migration scaffolded");
1266
+ return filePath;
1267
+ };
1268
+ return { up, down, status, create };
1269
+ };
1270
+
1271
+ // src/mongo-helpers.ts
1272
+ var withRetry = async (fn, options = {}, _ctx) => {
1273
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
1274
+ const delayMs = options.delayMs ?? 150;
1275
+ const logger = options.logger;
1276
+ let attempt = 0;
1277
+ let lastError;
1278
+ while (attempt < maxAttempts) {
1279
+ try {
1280
+ return await fn();
1281
+ } catch (err) {
1282
+ lastError = err;
1283
+ attempt += 1;
1284
+ if (attempt >= maxAttempts || !isMongoTransientError(err)) throw err;
1285
+ const backoff = delayMs * 2 ** (attempt - 1);
1286
+ logger?.debug?.({ attempt, backoffMs: backoff, error: err?.message }, "retrying");
1287
+ await sleep(backoff);
1288
+ }
1289
+ }
1290
+ throw lastError ?? new Error("withRetry: no attempt was made (maxAttempts must be >= 1)");
1291
+ };
1292
+ var withTransaction = async (client, fn) => {
1293
+ const session = client.startSession();
1294
+ try {
1295
+ session.startTransaction();
1296
+ const result = await fn(session);
1297
+ await session.commitTransaction();
1298
+ return result;
1299
+ } catch (err) {
1300
+ await session.abortTransaction().catch(() => {
1301
+ });
1302
+ throw err;
1303
+ } finally {
1304
+ await session.endSession().catch(() => {
1305
+ });
1306
+ }
1307
+ };
1308
+
1309
+ // src/shared/constants.ts
1310
+ var DEFAULT_MAX_LIMIT = 1e3;
1311
+ var DEFAULT_FIND_LIMIT = 100;
1312
+ var MAX_BATCH_OPS = 1e5;
1313
+
1314
+ // src/query-builder/query-builder.ts
1315
+ var QueryBuilder = class {
1316
+ constructor(ctx) {
1317
+ this.ctx = ctx;
1318
+ }
1319
+ _filter = {};
1320
+ _sort;
1321
+ _skip;
1322
+ _limit;
1323
+ _projection;
1324
+ _options = {};
1325
+ get filter() {
1326
+ return this._filter;
1327
+ }
1328
+ /** Shallow-merge a filter into the current one (later keys win). */
1329
+ where(filter) {
1330
+ this._filter = { ...this._filter, ...filter };
1331
+ return this;
1332
+ }
1333
+ /** AND-combine the current filter with a new one. */
1334
+ and(filter) {
1335
+ this._filter = { $and: [this._filter, filter] };
1336
+ return this;
1337
+ }
1338
+ /** OR-combine: current filter AND (filter1 OR filter2 ...). */
1339
+ or(...filters) {
1340
+ this._filter = { $and: [this._filter, { $or: filters }] };
1341
+ return this;
1342
+ }
1343
+ /** Set the sort order (`{ field: 1 | -1 }`). */
1344
+ sort(sort) {
1345
+ this._sort = sort;
1346
+ return this;
1347
+ }
1348
+ /** Skip the first `n` matching documents. */
1349
+ skip(n) {
1350
+ this._skip = n;
1351
+ return this;
1352
+ }
1353
+ /** Limit the result set to `n` documents. */
1354
+ limit(n) {
1355
+ this._limit = n;
1356
+ return this;
1357
+ }
1358
+ /** Project only the given fields — the projection is pushed to the driver. */
1359
+ select(fields) {
1360
+ const projection = {};
1361
+ for (const field of fields) projection[field] = 1;
1362
+ this._projection = projection;
1363
+ return this;
1364
+ }
1365
+ /** Raw projection document (`{ field: 1 }` / `{ field: 0 }`). */
1366
+ project(projection) {
1367
+ this._projection = projection;
1368
+ return this;
1369
+ }
1370
+ /** Set the index hint. */
1371
+ hint(hint) {
1372
+ this._options.hint = hint;
1373
+ return this;
1374
+ }
1375
+ /** Set the driver cursor batch size. */
1376
+ batchSize(n) {
1377
+ this._options.batchSize = n;
1378
+ return this;
1379
+ }
1380
+ /** Run the query inside a transaction session. */
1381
+ session(session) {
1382
+ this._options.session = session;
1383
+ return this;
1384
+ }
1385
+ /** Cap server-side execution time. */
1386
+ maxTimeMS(ms) {
1387
+ this._options.maxTimeMS = ms;
1388
+ return this;
1389
+ }
1390
+ /** Bypass the read cache for this query when `false`. */
1391
+ cache(on) {
1392
+ this._options.cache = on;
1393
+ return this;
1394
+ }
1395
+ /** Override in-flight dedup for this query when set. */
1396
+ dedupe(on) {
1397
+ this._options.dedupe = on;
1398
+ return this;
1399
+ }
1400
+ /** Compile the builder into driver options (does not execute). */
1401
+ compile() {
1402
+ const out = { ...this._options };
1403
+ if (this._sort) out.sort = this._sort;
1404
+ if (this._skip !== void 0) out.skip = this._skip;
1405
+ if (this._limit !== void 0) out.limit = this._limit;
1406
+ if (this._projection) out.projection = this._projection;
1407
+ return out;
1408
+ }
1409
+ countOptions() {
1410
+ const out = { ...this._options };
1411
+ if (this._skip !== void 0) out.skip = this._skip;
1412
+ if (this._limit !== void 0) out.limit = this._limit;
1413
+ return out;
1414
+ }
1415
+ /** Fetch one document (limit forced to 1). */
1416
+ async one() {
1417
+ const options = this.compile();
1418
+ if (options.limit === void 0) options.limit = 1;
1419
+ return this.ctx.run(
1420
+ "mongo.getOne",
1421
+ this._filter,
1422
+ (opts) => this.ctx.collection.findOne(
1423
+ this._filter,
1424
+ opts
1425
+ ),
1426
+ options
1427
+ );
1428
+ }
1429
+ /** Fetch many documents (default limit 100 when unset). */
1430
+ async many() {
1431
+ const options = this.compile();
1432
+ if (options.limit === void 0) options.limit = DEFAULT_FIND_LIMIT;
1433
+ return this.ctx.run(
1434
+ "mongo.findMany",
1435
+ this._filter,
1436
+ (opts) => this.ctx.collection.find(this._filter, opts).toArray(),
1437
+ options
1438
+ );
1439
+ }
1440
+ /** Streaming cursor (raw driver cursor; no retry, applies batchSize). */
1441
+ cursor() {
1442
+ return this.ctx.collection.find(
1443
+ this._filter,
1444
+ this.compile()
1445
+ );
1446
+ }
1447
+ /** Count matching documents. */
1448
+ async count() {
1449
+ return this.ctx.run(
1450
+ "mongo.countDocuments",
1451
+ this._filter,
1452
+ (opts) => this.ctx.collection.countDocuments(this._filter, opts),
1453
+ this.countOptions()
1454
+ );
1455
+ }
1456
+ /** True when at least one document matches. */
1457
+ async exists() {
1458
+ return await this.one() !== null;
1459
+ }
1460
+ };
1461
+
1462
+ // src/relation/relation.ts
1463
+ var belongsTo = (config) => ({
1464
+ type: "belongsTo",
1465
+ foreignField: "_id",
1466
+ ...config
1467
+ });
1468
+ var hasMany = (config) => ({
1469
+ type: "hasMany",
1470
+ foreignField: "_id",
1471
+ ...config
1472
+ });
1473
+ var manyToMany = (config) => ({
1474
+ type: "manyToMany",
1475
+ foreignField: "_id",
1476
+ ...config
1477
+ });
1478
+
1479
+ // src/repository/repository.ts
1480
+ var createRepository = (manager, collection, options = {}) => {
1481
+ const repo = {
1482
+ collection,
1483
+ manager,
1484
+ getById: (id, o) => manager.getOne(collection, { _id: id }, o),
1485
+ getByIds: (ids, o) => manager.findMany(collection, { _id: { $in: ids } }, o),
1486
+ findOne: (filter, o) => manager.getOne(collection, filter, o),
1487
+ findOneOrFail: (filter, o) => manager.getOneOrFail(collection, filter, o),
1488
+ findMany: (filter, o) => manager.findMany(collection, filter, {
1489
+ ...o ?? {},
1490
+ ...options.limit !== void 0 ? { limit: options.limit } : {}
1491
+ }),
1492
+ findActive: (filter, o) => manager.findActive(collection, filter, o),
1493
+ count: (filter, o) => manager.countDocuments(collection, filter, o),
1494
+ exists: async (filter, o) => await manager.getOne(collection, filter, { ...o ?? {}, projection: { _id: 1 } }) !== null,
1495
+ distinct: (field, filter, o) => manager.distinct(collection, field, filter, o),
1496
+ create: (doc, o) => manager.insertOne(collection, doc, o),
1497
+ createMany: (docs, o) => manager.insertMany(collection, docs, o),
1498
+ update: (id, patch, o) => manager.updateOne(collection, { _id: id }, patch, o),
1499
+ updateVersioned: (id, patch, o) => manager.updateWithVersion(collection, { _id: id }, patch, o),
1500
+ deleteById: (id, o) => manager.deleteOne(collection, { _id: id }, o),
1501
+ softDelete: (id, o) => manager.softDeleteOne(collection, { _id: id }, o),
1502
+ page: (filter, config) => manager.paginateFlexible(collection, filter, config),
1503
+ pageCursor: (filter, config) => manager.paginateCursor(collection, filter, config),
1504
+ pipeline: (o) => manager.pipeline(collection, o),
1505
+ aggregate: (stages, o) => manager.aggregate(collection, stages, o),
1506
+ // The repository's public signature already validates `relations` at the
1507
+ // call site; the manager's mapped-type param can't be re-inferred here
1508
+ // (TS mapped-type inference friction), so delegate through the validated
1509
+ // shape without weakening the public type.
1510
+ populate: (docs, relations, o) => manager.populate(docs, relations, o),
1511
+ watch: (pipeline, options2) => manager.watchCollection(collection, pipeline, options2)
1512
+ };
1513
+ return repo;
1514
+ };
1515
+
1516
+ // src/schema/collections.ts
1517
+ var resolveItemName = (item) => "schema" in item ? item.schema.name : item.name;
1518
+ var resolveItemValue = (item) => {
1519
+ if ("schema" in item) {
1520
+ const def = item;
1521
+ return {
1522
+ schema: def.schema,
1523
+ ...def.indexes ? { indexes: def.indexes } : {},
1524
+ ...def.hooks ? { hooks: def.hooks } : {},
1525
+ ...def.timestamps !== void 0 ? { timestamps: def.timestamps } : {}
1526
+ };
1527
+ }
1528
+ return item;
1529
+ };
1530
+ var defineCollection = (name, schema, extras = {}) => {
1531
+ const namedSchema = { ...schema, name };
1532
+ return {
1533
+ name,
1534
+ schema: namedSchema,
1535
+ ...extras.indexes ? { indexes: extras.indexes } : {},
1536
+ ...extras.hooks ? { hooks: extras.hooks } : {},
1537
+ ...extras.timestamps !== void 0 ? { timestamps: extras.timestamps } : {}
1538
+ };
1539
+ };
1540
+ var defineCollections = (...items) => {
1541
+ const out = {};
1542
+ for (const item of items) {
1543
+ const name = resolveItemName(item);
1544
+ if (name in out) {
1545
+ throw new Error(`defineCollections: duplicate collection name "${name}"`);
1546
+ }
1547
+ out[name] = resolveItemValue(item);
1548
+ }
1549
+ return out;
1550
+ };
1551
+
1552
+ // src/schema/to-mongo-schema.ts
1553
+ var addNumericBounds = (out, field) => {
1554
+ if (field.minimum !== void 0) out.minimum = field.minimum;
1555
+ if (field.maximum !== void 0) out.maximum = field.maximum;
1556
+ if (field.multipleOf !== void 0) out.multipleOf = field.multipleOf;
1557
+ if (field.exclusiveMinimum) out.exclusiveMinimum = true;
1558
+ if (field.exclusiveMaximum) out.exclusiveMaximum = true;
1559
+ return out;
1560
+ };
1561
+ var convertField = (field) => {
1562
+ if (field.kind === "raw") {
1563
+ const out2 = { ...field.fragment };
1564
+ if (field.description && !out2.description) out2.description = field.description;
1565
+ return out2;
1566
+ }
1567
+ const out = {};
1568
+ if (field.description) out.description = field.description;
1569
+ switch (field.kind) {
1570
+ case "string":
1571
+ out.bsonType = "string";
1572
+ if (field.minLength !== void 0) out.minLength = field.minLength;
1573
+ if (field.maxLength !== void 0) out.maxLength = field.maxLength;
1574
+ if (field.pattern) out.pattern = field.pattern;
1575
+ return out;
1576
+ case "number":
1577
+ out.bsonType = field.integer ? "int" : "number";
1578
+ return addNumericBounds(out, field);
1579
+ case "double":
1580
+ out.bsonType = "double";
1581
+ return addNumericBounds(out, field);
1582
+ case "long":
1583
+ out.bsonType = "long";
1584
+ return addNumericBounds(out, field);
1585
+ case "decimal":
1586
+ out.bsonType = "decimal";
1587
+ return addNumericBounds(out, field);
1588
+ case "boolean":
1589
+ out.bsonType = "bool";
1590
+ return out;
1591
+ case "date":
1592
+ out.bsonType = "date";
1593
+ return out;
1594
+ case "objectId":
1595
+ out.bsonType = "objectId";
1596
+ return out;
1597
+ case "geoPoint":
1598
+ return {
1599
+ bsonType: "object",
1600
+ required: ["type", "coordinates"],
1601
+ properties: {
1602
+ type: { enum: ["Point"] },
1603
+ coordinates: {
1604
+ bsonType: "array",
1605
+ items: { bsonType: "number" },
1606
+ minItems: 2,
1607
+ maxItems: 2
1608
+ }
1609
+ }
1610
+ };
1611
+ case "null":
1612
+ out.bsonType = "null";
1613
+ return out;
1614
+ case "any":
1615
+ return out;
1616
+ case "enum": {
1617
+ out.enum = [...field.values];
1618
+ const types = /* @__PURE__ */ new Set();
1619
+ for (const v of field.values) {
1620
+ if (v === null) types.add("null");
1621
+ else if (typeof v === "string") types.add("string");
1622
+ else if (typeof v === "number") {
1623
+ const allInt = field.values.every((x) => typeof x !== "number" || Number.isInteger(x));
1624
+ types.add(allInt ? "int" : "number");
1625
+ break;
1626
+ }
1627
+ }
1628
+ if (types.size === 1) {
1629
+ out.bsonType = [...types][0];
1630
+ } else if (types.size > 1) {
1631
+ out.bsonType = [...types];
1632
+ }
1633
+ return out;
1634
+ }
1635
+ case "array":
1636
+ out.bsonType = "array";
1637
+ out.items = convertField(field.items);
1638
+ if (field.minItems !== void 0) out.minItems = field.minItems;
1639
+ if (field.maxItems !== void 0) out.maxItems = field.maxItems;
1640
+ if (field.uniqueItems) out.uniqueItems = true;
1641
+ return out;
1642
+ case "object":
1643
+ return convertObject(field);
1644
+ }
1645
+ };
1646
+ var convertObject = (field) => {
1647
+ const out = { bsonType: "object" };
1648
+ const required = [];
1649
+ const properties = {};
1650
+ for (const [key, prop] of Object.entries(field.properties)) {
1651
+ properties[key] = convertField(prop);
1652
+ if (!prop.flags.optional && !prop.flags.hasDefault) required.push(key);
1653
+ }
1654
+ if (required.length > 0) out.required = required;
1655
+ out.properties = properties;
1656
+ out.additionalProperties = field.additionalProperties ?? false;
1657
+ if (field.minProperties !== void 0) out.minProperties = field.minProperties;
1658
+ if (field.maxProperties !== void 0) out.maxProperties = field.maxProperties;
1659
+ return out;
1660
+ };
1661
+ var ORM_RESERVED_FIELDS = {
1662
+ __v: { bsonType: "int" },
1663
+ deletedAt: { bsonType: ["date", "null"] }
1664
+ };
1665
+ var toMongoSchema = (schema) => {
1666
+ const result = convertObject(schema);
1667
+ result.properties ??= {};
1668
+ if (!result.properties._id) {
1669
+ result.properties._id = { bsonType: "objectId" };
1670
+ }
1671
+ for (const [key, fragment] of Object.entries(ORM_RESERVED_FIELDS)) {
1672
+ if (!result.properties[key]) result.properties[key] = fragment;
1673
+ }
1674
+ return result;
1675
+ };
1676
+ var toMongoValidator = (schema) => ({
1677
+ $jsonSchema: toMongoSchema(schema)
1678
+ });
1679
+
1680
+ // src/schema/types.ts
1681
+ var chain = (field) => {
1682
+ const obj = field;
1683
+ obj.optional = () => chain({ ...field, flags: { ...field.flags, optional: true } });
1684
+ obj.default = (value) => chain({ ...field, flags: { ...field.flags, hasDefault: true, defaultValue: value } });
1685
+ return obj;
1686
+ };
1687
+ var BASE = { flags: { optional: false, hasDefault: false } };
1688
+ function objectField(properties, opts = {}) {
1689
+ return chain({ kind: "object", properties, ...BASE, ...opts });
1690
+ }
1691
+ var s = {
1692
+ /** UTF-8 string field (optional `format` / length / regex constraints). */
1693
+ string(opts = {}) {
1694
+ return chain({ kind: "string", ...BASE, ...opts });
1695
+ },
1696
+ /** 32-bit integer field with optional bounds (min/max, exclusive, multipleOf). */
1697
+ integer(opts = {}) {
1698
+ return chain({ kind: "number", integer: true, ...BASE, ...opts });
1699
+ },
1700
+ /** Double/float field with optional bounds (min/max, exclusive, multipleOf). */
1701
+ number(opts = {}) {
1702
+ return chain({ kind: "number", integer: false, ...BASE, ...opts });
1703
+ },
1704
+ /** Strict BSON `double` (IEEE 754 float64) field. */
1705
+ double(opts = {}) {
1706
+ return chain({ kind: "double", ...BASE, ...opts });
1707
+ },
1708
+ /** Strict BSON `long` (int64) field — inferred as `number`. */
1709
+ long(opts = {}) {
1710
+ return chain({ kind: "long", ...BASE, ...opts });
1711
+ },
1712
+ /** Strict BSON `decimal` (Decimal128) field — inferred as `Decimal128`. */
1713
+ decimal(opts = {}) {
1714
+ return chain({ kind: "decimal", ...BASE, ...opts });
1715
+ },
1716
+ /** Boolean field. */
1717
+ boolean() {
1718
+ return chain({ kind: "boolean", ...BASE });
1719
+ },
1720
+ /** UTC datetime field (BSON date). */
1721
+ date() {
1722
+ return chain({ kind: "date", ...BASE });
1723
+ },
1724
+ /** MongoDB ObjectId field (`_id` and foreign keys). */
1725
+ objectId() {
1726
+ return chain({ kind: "objectId", ...BASE });
1727
+ },
1728
+ /** Array of `items` (nested schema types allowed), with optional length/uniqueness. */
1729
+ array(items, opts = {}) {
1730
+ const field = { kind: "array", items, ...BASE, ...opts };
1731
+ return chain(field);
1732
+ },
1733
+ object: objectField,
1734
+ /** Enum union of literal string/number values. */
1735
+ enum(values) {
1736
+ const field = { kind: "enum", values, ...BASE };
1737
+ return chain(field);
1738
+ },
1739
+ /** Unconstrained field (no validator bounds). */
1740
+ any() {
1741
+ return chain({ kind: "any", ...BASE });
1742
+ },
1743
+ /** Explicitly-nullable field. */
1744
+ null() {
1745
+ return chain({ kind: "null", ...BASE });
1746
+ },
1747
+ /** GeoJSON Point field (type `Point` + `[lng, lat]` coordinates) for `2dsphere` queries. */
1748
+ geoPoint() {
1749
+ return chain({ kind: "geoPoint", ...BASE });
1750
+ },
1751
+ /**
1752
+ * Raw `$jsonSchema` escape hatch: pass an arbitrary fragment through verbatim
1753
+ * to the validator. Use for Mongo keywords the DSL doesn't model (e.g.
1754
+ * `patternProperties`, `allOf`, `anyOf`, `dependencies`).
1755
+ */
1756
+ jsonSchema(fragment) {
1757
+ return chain({ kind: "raw", fragment, ...BASE });
1758
+ }
1759
+ };
1760
+ var optional = (field) => chain({ ...field, flags: { ...field.flags, optional: true } });
1761
+ var withDefault = (field, value) => chain({ ...field, flags: { ...field.flags, hasDefault: true, defaultValue: value } });
1762
+ var push = (issues, path, code, expected, message) => {
1763
+ issues.push({ path, code, expected, message });
1764
+ };
1765
+ var describeValue = (value) => {
1766
+ if (value instanceof ObjectId) return "ObjectId";
1767
+ if (value instanceof Date) return "Date";
1768
+ if (value === null) return "null";
1769
+ if (Array.isArray(value)) return "array";
1770
+ return typeof value;
1771
+ };
1772
+ var isPlainObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof ObjectId) && !(value instanceof RegExp);
1773
+ var numericValue = (value) => {
1774
+ if (value instanceof Decimal128) {
1775
+ const n = Number(value.toString());
1776
+ return Number.isNaN(n) ? void 0 : n;
1777
+ }
1778
+ if (typeof value === "number" && !Number.isNaN(value)) return value;
1779
+ return void 0;
1780
+ };
1781
+ var checkNumericBounds = (field, value, path, issues) => {
1782
+ const num = numericValue(value);
1783
+ if (num === void 0) return;
1784
+ if (field.minimum !== void 0 && (field.exclusiveMinimum ? num <= field.minimum : num < field.minimum)) {
1785
+ push(
1786
+ issues,
1787
+ path,
1788
+ "constraint",
1789
+ `minimum ${field.minimum}${field.exclusiveMinimum ? " (exclusive)" : ""}`,
1790
+ `${num} out of range`
1791
+ );
1792
+ }
1793
+ if (field.maximum !== void 0 && (field.exclusiveMaximum ? num >= field.maximum : num > field.maximum)) {
1794
+ push(
1795
+ issues,
1796
+ path,
1797
+ "constraint",
1798
+ `maximum ${field.maximum}${field.exclusiveMaximum ? " (exclusive)" : ""}`,
1799
+ `${num} out of range`
1800
+ );
1801
+ }
1802
+ if (field.multipleOf !== void 0 && field.multipleOf > 0 && num % field.multipleOf !== 0) {
1803
+ push(
1804
+ issues,
1805
+ path,
1806
+ "constraint",
1807
+ `multipleOf ${field.multipleOf}`,
1808
+ `${num} is not a multiple of ${field.multipleOf}`
1809
+ );
1810
+ }
1811
+ };
1812
+ var bsonTypeMatches = (bsonType, value) => {
1813
+ switch (bsonType) {
1814
+ case "objectId":
1815
+ return value instanceof ObjectId;
1816
+ case "int":
1817
+ return typeof value === "number" && Number.isInteger(value);
1818
+ case "number":
1819
+ return typeof value === "number" && !Number.isNaN(value);
1820
+ case "string":
1821
+ return typeof value === "string";
1822
+ case "bool":
1823
+ return typeof value === "boolean";
1824
+ case "date":
1825
+ return value instanceof Date;
1826
+ case "null":
1827
+ return value === null;
1828
+ case "array":
1829
+ return Array.isArray(value);
1830
+ case "object":
1831
+ return isPlainObject2(value);
1832
+ case "double":
1833
+ return typeof value === "number" && !Number.isNaN(value);
1834
+ case "long":
1835
+ return typeof value === "number" && Number.isInteger(value);
1836
+ case "decimal":
1837
+ return value instanceof Decimal128;
1838
+ default:
1839
+ return true;
1840
+ }
1841
+ };
1842
+ var checkReservedValue = (key, value, path, issues) => {
1843
+ const fragment = ORM_RESERVED_FIELDS[key];
1844
+ if (!fragment) return;
1845
+ const types = Array.isArray(fragment.bsonType) ? fragment.bsonType : [fragment.bsonType];
1846
+ if (types.some((t) => bsonTypeMatches(t, value))) return;
1847
+ push(
1848
+ issues,
1849
+ path,
1850
+ "type",
1851
+ types.join(" | "),
1852
+ `Reserved field "${key}" must be ${types.join(" | ")}`
1853
+ );
1854
+ };
1855
+
1856
+ // src/schema/validate-doc/field-validators.ts
1857
+ var stringValidator = (_deps, field, value, path, issues) => {
1858
+ const f = field;
1859
+ if (typeof value !== "string") {
1860
+ push(issues, path, "type", "string", `Expected string, got ${describeValue(value)}`);
1861
+ return;
1862
+ }
1863
+ if (f.minLength !== void 0 && value.length < f.minLength) {
1864
+ push(
1865
+ issues,
1866
+ path,
1867
+ "constraint",
1868
+ `string(minLength ${f.minLength})`,
1869
+ `Length ${value.length} < ${f.minLength}`
1870
+ );
1871
+ }
1872
+ if (f.maxLength !== void 0 && value.length > f.maxLength) {
1873
+ push(
1874
+ issues,
1875
+ path,
1876
+ "constraint",
1877
+ `string(maxLength ${f.maxLength})`,
1878
+ `Length ${value.length} > ${f.maxLength}`
1879
+ );
1880
+ }
1881
+ if (f.pattern) {
1882
+ try {
1883
+ if (!new RegExp(f.pattern).test(value)) {
1884
+ push(
1885
+ issues,
1886
+ path,
1887
+ "constraint",
1888
+ `string(pattern ${f.pattern})`,
1889
+ `"${value}" does not match pattern`
1890
+ );
1891
+ }
1892
+ } catch {
1893
+ }
1894
+ }
1895
+ };
1896
+ var numberValidator = (_deps, field, value, path, issues) => {
1897
+ const f = field;
1898
+ const expected = f.integer ? "integer" : "number";
1899
+ if (typeof value !== "number" || Number.isNaN(value)) {
1900
+ push(issues, path, "type", expected, `Expected ${expected}, got ${describeValue(value)}`);
1901
+ return;
1902
+ }
1903
+ if (f.integer && !Number.isInteger(value)) {
1904
+ push(issues, path, "type", "integer", `Expected integer, got ${value}`);
1905
+ }
1906
+ checkNumericBounds(f, value, path, issues);
1907
+ };
1908
+ var floatingValidator = (_deps, field, value, path, issues) => {
1909
+ const f = field;
1910
+ const ok = f.kind === "decimal" ? value instanceof Decimal128 : f.kind === "long" ? typeof value === "number" && Number.isInteger(value) : typeof value === "number" && !Number.isNaN(value);
1911
+ if (!ok) {
1912
+ push(issues, path, "type", f.kind, `Expected ${f.kind}, got ${describeValue(value)}`);
1913
+ return;
1914
+ }
1915
+ checkNumericBounds(f, value, path, issues);
1916
+ };
1917
+ var booleanValidator = (_deps, _field, value, path, issues) => {
1918
+ if (typeof value !== "boolean") {
1919
+ push(issues, path, "type", "boolean", `Expected boolean, got ${describeValue(value)}`);
1920
+ }
1921
+ };
1922
+ var dateValidator = (_deps, _field, value, path, issues) => {
1923
+ if (!(value instanceof Date)) {
1924
+ push(issues, path, "type", "date", `Expected Date, got ${describeValue(value)}`);
1925
+ }
1926
+ };
1927
+ var objectIdValidator = (_deps, _field, value, path, issues) => {
1928
+ if (!(value instanceof ObjectId)) {
1929
+ push(issues, path, "type", "ObjectId", `Expected ObjectId, got ${describeValue(value)}`);
1930
+ }
1931
+ };
1932
+ var geoPointValidator = (_deps, _field, value, path, issues) => {
1933
+ if (!isPlainObject2(value)) {
1934
+ push(issues, path, "type", "GeoPoint", `Expected GeoPoint object, got ${describeValue(value)}`);
1935
+ return;
1936
+ }
1937
+ const gp = value;
1938
+ if (gp.type !== "Point") {
1939
+ push(
1940
+ issues,
1941
+ path,
1942
+ "enum",
1943
+ 'GeoPoint(type "Point")',
1944
+ `Expected type "Point", got ${JSON.stringify(gp.type)}`
1945
+ );
1946
+ }
1947
+ const coords = gp.coordinates;
1948
+ if (!Array.isArray(coords) || coords.length !== 2 || !coords.every((c) => typeof c === "number" && Number.isFinite(c))) {
1949
+ push(
1950
+ issues,
1951
+ path,
1952
+ "constraint",
1953
+ "GeoPoint(coordinates: [lng, lat])",
1954
+ "Expected [lng, lat] pair of finite numbers"
1955
+ );
1956
+ }
1957
+ };
1958
+ var nullValidator = (_deps, _field, value, path, issues) => {
1959
+ if (value !== null) {
1960
+ push(issues, path, "type", "null", `Expected null, got ${describeValue(value)}`);
1961
+ }
1962
+ };
1963
+ var anyValidator = () => {
1964
+ };
1965
+ var rawValidator = () => {
1966
+ };
1967
+ var enumValidator = (_deps, field, value, path, issues) => {
1968
+ const f = field;
1969
+ if (!f.values.includes(value)) {
1970
+ push(
1971
+ issues,
1972
+ path,
1973
+ "enum",
1974
+ `enum[${f.values.join(", ")}]`,
1975
+ `Value ${JSON.stringify(value)} is not in the allowed set`
1976
+ );
1977
+ }
1978
+ };
1979
+ var arrayValidator = (deps2, field, value, path, issues) => {
1980
+ const f = field;
1981
+ if (!Array.isArray(value)) {
1982
+ push(
1983
+ issues,
1984
+ path,
1985
+ "type",
1986
+ `array<${f.items.kind}>`,
1987
+ `Expected array, got ${describeValue(value)}`
1988
+ );
1989
+ return;
1990
+ }
1991
+ if (f.minItems !== void 0 && value.length < f.minItems) {
1992
+ push(
1993
+ issues,
1994
+ path,
1995
+ "constraint",
1996
+ `array(minItems ${f.minItems})`,
1997
+ `Length ${value.length} < ${f.minItems}`
1998
+ );
1999
+ }
2000
+ if (f.maxItems !== void 0 && value.length > f.maxItems) {
2001
+ push(
2002
+ issues,
2003
+ path,
2004
+ "constraint",
2005
+ `array(maxItems ${f.maxItems})`,
2006
+ `Length ${value.length} > ${f.maxItems}`
2007
+ );
2008
+ }
2009
+ if (f.uniqueItems) {
2010
+ const seen = new Set(
2011
+ value.map(
2012
+ (item) => item instanceof ObjectId ? item.toHexString() : typeof item === "object" && item !== null ? JSON.stringify(item) : item
2013
+ )
2014
+ );
2015
+ if (seen.size !== value.length) {
2016
+ push(issues, path, "constraint", "array(uniqueItems)", "Array contains duplicate items");
2017
+ }
2018
+ }
2019
+ value.forEach((item, i) => {
2020
+ deps2.validateValue(f.items, item, `${path}[${i}]`, issues);
2021
+ });
2022
+ };
2023
+ var objectValidator = (deps2, field, value, path, issues) => {
2024
+ const f = field;
2025
+ if (!isPlainObject2(value)) {
2026
+ push(issues, path, "type", "object", `Expected object, got ${describeValue(value)}`);
2027
+ return;
2028
+ }
2029
+ deps2.validateObjectProps(f, value, path, issues, false);
2030
+ };
2031
+ var FIELD_VALIDATORS = {
2032
+ string: stringValidator,
2033
+ number: numberValidator,
2034
+ double: floatingValidator,
2035
+ long: floatingValidator,
2036
+ decimal: floatingValidator,
2037
+ boolean: booleanValidator,
2038
+ date: dateValidator,
2039
+ objectId: objectIdValidator,
2040
+ geoPoint: geoPointValidator,
2041
+ null: nullValidator,
2042
+ any: anyValidator,
2043
+ raw: rawValidator,
2044
+ enum: enumValidator,
2045
+ array: arrayValidator,
2046
+ object: objectValidator
2047
+ };
2048
+
2049
+ // src/schema/validate-doc/validate.ts
2050
+ function validateValue(field, value, path, issues) {
2051
+ const validator = FIELD_VALIDATORS[field.kind];
2052
+ if (validator) validator(deps, field, value, path, issues);
2053
+ }
2054
+ function validateObjectProps(field, value, path, issues, topLevel) {
2055
+ const propPath = (key) => path ? `${path}.${key}` : key;
2056
+ for (const [key, prop] of Object.entries(field.properties)) {
2057
+ const present = key in value && value[key] !== void 0;
2058
+ if (!present) {
2059
+ if (!prop.flags.optional && !prop.flags.hasDefault) {
2060
+ push(issues, propPath(key), "missing", prop.kind, `Required field "${key}" is missing`);
2061
+ }
2062
+ continue;
2063
+ }
2064
+ validateValue(prop, value[key], propPath(key), issues);
2065
+ }
2066
+ if (field.additionalProperties !== true) {
2067
+ const allowedReserved = topLevel ? /* @__PURE__ */ new Set(["_id", ...Object.keys(ORM_RESERVED_FIELDS)]) : /* @__PURE__ */ new Set();
2068
+ for (const key of Object.keys(value)) {
2069
+ if (key in field.properties) continue;
2070
+ if (allowedReserved.has(key)) continue;
2071
+ push(
2072
+ issues,
2073
+ propPath(key),
2074
+ "unknown_key",
2075
+ "no additional properties",
2076
+ `Unexpected field "${key}"`
2077
+ );
2078
+ }
2079
+ }
2080
+ const propCount = Object.keys(value).length;
2081
+ if (field.minProperties !== void 0 && propCount < field.minProperties) {
2082
+ push(
2083
+ issues,
2084
+ path,
2085
+ "constraint",
2086
+ `object(minProperties ${field.minProperties})`,
2087
+ `Has ${propCount} properties < ${field.minProperties}`
2088
+ );
2089
+ }
2090
+ if (field.maxProperties !== void 0 && propCount > field.maxProperties) {
2091
+ push(
2092
+ issues,
2093
+ path,
2094
+ "constraint",
2095
+ `object(maxProperties ${field.maxProperties})`,
2096
+ `Has ${propCount} properties > ${field.maxProperties}`
2097
+ );
2098
+ }
2099
+ if (topLevel) {
2100
+ if ("_id" in value && !("_id" in field.properties) && value._id !== void 0) {
2101
+ if (!(value._id instanceof ObjectId)) {
2102
+ push(issues, "_id", "type", "ObjectId", 'Reserved field "_id" must be ObjectId');
2103
+ }
2104
+ }
2105
+ for (const key of Object.keys(ORM_RESERVED_FIELDS)) {
2106
+ if (key in field.properties) continue;
2107
+ if (key in value && value[key] !== void 0) {
2108
+ checkReservedValue(key, value[key], key, issues);
2109
+ }
2110
+ }
2111
+ }
2112
+ }
2113
+ var deps = { validateValue, validateObjectProps };
2114
+ var validateDoc = (schema, doc) => {
2115
+ const issues = [];
2116
+ if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
2117
+ push(issues, "", "type", "object", `Expected a document object, got ${describeValue(doc)}`);
2118
+ return issues;
2119
+ }
2120
+ validateObjectProps(schema, doc, "", issues, true);
2121
+ return issues;
2122
+ };
2123
+
2124
+ // src/service/collection-name.ts
2125
+ var DEFAULT_COLLECTION_PREFIX_SEPARATOR = "-";
2126
+ var createResolveCollectionName = (config) => {
2127
+ const sep = config.collectionPrefixSeparator ?? DEFAULT_COLLECTION_PREFIX_SEPARATOR;
2128
+ return (logical) => {
2129
+ const override = config.collectionPhysicalNames?.[logical];
2130
+ if (override) return override;
2131
+ const prefix = config.collectionPrefix;
2132
+ if (!prefix) return logical;
2133
+ return `${prefix}${sep}${logical}`;
2134
+ };
2135
+ };
2136
+
2137
+ // src/service/connection-uri.ts
2138
+ var normalizeMongoUrl = (rawUrl) => {
2139
+ try {
2140
+ const url = new URL(rawUrl);
2141
+ if (!url.searchParams.has("retryWrites")) url.searchParams.set("retryWrites", "false");
2142
+ const hasWriteConcern = url.searchParams.has("w") || url.searchParams.has("wtimeoutMS") || url.searchParams.has("writeConcern");
2143
+ if (!hasWriteConcern) url.searchParams.set("w", "1");
2144
+ const hostCount = url.hostname.split(",").length;
2145
+ if (hostCount === 1 && !url.searchParams.has("replicaSet") && !url.searchParams.has("directConnection")) {
2146
+ url.searchParams.set("directConnection", "true");
2147
+ }
2148
+ return url.toString();
2149
+ } catch {
2150
+ return rawUrl;
2151
+ }
2152
+ };
2153
+
2154
+ // src/service/cache-invalidation.ts
2155
+ var SEP2 = "\0";
2156
+ var CacheInvalidator = class {
2157
+ cache;
2158
+ logger;
2159
+ streams = /* @__PURE__ */ new Map();
2160
+ disabled = false;
2161
+ stopped = true;
2162
+ warnedUnsupported = false;
2163
+ constructor(options) {
2164
+ this.cache = options.cache;
2165
+ this.logger = options.logger;
2166
+ }
2167
+ /** Number of currently open change streams. */
2168
+ get size() {
2169
+ return this.streams.size;
2170
+ }
2171
+ /**
2172
+ * Open one watcher per unique (database, physical collection). Idempotent:
2173
+ * already-open streams are skipped; after `stop()` a later `start()` reopens.
2174
+ * Never throws — failures are logged (on unsupported servers it disables once).
2175
+ */
2176
+ async start(refs) {
2177
+ if (this.disabled) return;
2178
+ this.stopped = false;
2179
+ const seen = /* @__PURE__ */ new Set();
2180
+ for (const ref of refs) {
2181
+ const key = `${ref.db.databaseName}${SEP2}${ref.collection}`;
2182
+ if (seen.has(key) || this.streams.has(key)) continue;
2183
+ seen.add(key);
2184
+ void this.watchLoop(key, ref);
2185
+ }
2186
+ }
2187
+ /** Close every watcher. Safe to call repeatedly, and before any `start`. */
2188
+ async stop() {
2189
+ this.stopped = true;
2190
+ const closes = [...this.streams.values()].map((stream) => {
2191
+ try {
2192
+ return Promise.resolve(stream.close()).catch(() => {
2193
+ });
2194
+ } catch {
2195
+ return Promise.resolve();
2196
+ }
2197
+ });
2198
+ this.streams.clear();
2199
+ await Promise.allSettled(closes);
2200
+ }
2201
+ async watchLoop(key, ref) {
2202
+ let backoff = 0;
2203
+ while (!this.stopped) {
2204
+ try {
2205
+ const stream = ref.db.collection(ref.collection).watch([]);
2206
+ stream.on("error", () => {
2207
+ });
2208
+ this.streams.set(key, stream);
2209
+ if (backoff > 0) {
2210
+ this.cache.invalidateByCollection(
2211
+ cacheCollectionKey(ref.db.databaseName, ref.collection)
2212
+ );
2213
+ }
2214
+ const iterator = stream[Symbol.asyncIterator]();
2215
+ for await (const _change of iterator) {
2216
+ this.cache.invalidateByCollection(
2217
+ cacheCollectionKey(ref.db.databaseName, ref.collection)
2218
+ );
2219
+ }
2220
+ } catch (err) {
2221
+ const message = err instanceof Error ? err.message : String(err);
2222
+ const abandoned = this.streams.get(key);
2223
+ this.streams.delete(key);
2224
+ if (abandoned) {
2225
+ try {
2226
+ await abandoned.close();
2227
+ } catch {
2228
+ }
2229
+ }
2230
+ if (/only supported on replica sets/i.test(message)) {
2231
+ this.disabled = true;
2232
+ if (!this.warnedUnsupported) {
2233
+ this.warnedUnsupported = true;
2234
+ this.logger.warn?.(
2235
+ { collection: ref.collection, error: message },
2236
+ "cacheWatch: change streams are not supported here (standalone?) \u2014 the query cache stays write-through only; set cache: { ttlMs } or disable cacheWatch"
2237
+ );
2238
+ }
2239
+ return;
2240
+ }
2241
+ if (this.stopped) return;
2242
+ this.logger.warn?.(
2243
+ { error: message, collection: ref.collection },
2244
+ "cacheWatch: change stream error, retrying with backoff"
2245
+ );
2246
+ backoff = Math.min(backoff + 1e3, 5e3);
2247
+ await sleepJittered(backoff);
2248
+ }
2249
+ }
2250
+ }
2251
+ };
2252
+
2253
+ // src/utils/logger.ts
2254
+ var createNoopLogger = () => ({
2255
+ debug: () => {
2256
+ },
2257
+ info: () => {
2258
+ },
2259
+ warn: () => {
2260
+ },
2261
+ error: () => {
2262
+ }
2263
+ });
2264
+ var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
2265
+ var fmtObj = (obj) => {
2266
+ try {
2267
+ return JSON.stringify(obj);
2268
+ } catch {
2269
+ return String(obj);
2270
+ }
2271
+ };
2272
+ var createConsoleLogger = (level = "info") => {
2273
+ const threshold = LEVEL_RANK[level];
2274
+ const make = (method) => {
2275
+ const rank = LEVEL_RANK[method];
2276
+ const fn = (a, b) => {
2277
+ if (rank < threshold) return;
2278
+ const obj = typeof a === "string" ? void 0 : a;
2279
+ const msg = typeof a === "string" ? a : b;
2280
+ const prefix = obj !== void 0 ? fmtObj(obj) : "";
2281
+ if (msg && prefix) {
2282
+ console[method](`[${method.toUpperCase()}]`, prefix, msg);
2283
+ } else {
2284
+ console[method](`[${method.toUpperCase()}]`, msg ?? prefix);
2285
+ }
2286
+ };
2287
+ return fn;
2288
+ };
2289
+ return {
2290
+ debug: make("debug"),
2291
+ info: make("info"),
2292
+ warn: make("warn"),
2293
+ error: make("error")
2294
+ };
2295
+ };
2296
+
2297
+ // src/service/config.ts
2298
+ var resolveCache = (value) => {
2299
+ if (value === null) return void 0;
2300
+ if (value === void 0) return new QueryCache();
2301
+ if (value instanceof QueryCache) return value;
2302
+ return new QueryCache(value);
2303
+ };
2304
+ var resolveRuntimeConfig = (configOverrides) => {
2305
+ const config = {
2306
+ defaultDb: configOverrides.defaultDb ?? process.env.MONGO_URL ?? "mongodb://localhost:27017/",
2307
+ migrationDir: configOverrides.migrationDir ?? process.env.MIGRATION_DIR ?? "./migrations",
2308
+ appName: configOverrides.appName ?? process.env.APP_NAME ?? "unknown-app"
2309
+ };
2310
+ const logger = configOverrides.logger ?? createConsoleLogger();
2311
+ const perfEnabled = configOverrides.perf !== false;
2312
+ const sharedCache = perfEnabled ? resolveCache(configOverrides.cache) : void 0;
2313
+ const dedupeReads = perfEnabled ? configOverrides.dedupeReads ?? true : false;
2314
+ const wrapMongoErrors = configOverrides.wrapMongoErrors !== false;
2315
+ return { config, logger, perfEnabled, sharedCache, dedupeReads, wrapMongoErrors };
2316
+ };
2317
+ var makeGetDbUrl = (dbClients, config) => (name) => {
2318
+ const clientConfig = dbClients[name];
2319
+ if (clientConfig?.dbUrl) return normalizeMongoUrl(clientConfig.dbUrl);
2320
+ const envName = `DB_${name.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}`;
2321
+ const envUrl = process.env[envName];
2322
+ if (envUrl) return normalizeMongoUrl(envUrl);
2323
+ return normalizeMongoUrl(config.defaultDb);
2324
+ };
2325
+ var makeConnectionFactory = (dbClients, db, getDbUrl, buildManager2, openedClients = /* @__PURE__ */ new Map()) => {
2326
+ return async (names) => {
2327
+ const keys = names && names.length > 0 ? names : Object.keys(dbClients);
2328
+ const openedThisCall = [];
2329
+ const assignedThisCall = [];
2330
+ try {
2331
+ for (const key of keys) {
2332
+ const definition = dbClients[key];
2333
+ const dbUrl = getDbUrl(key);
2334
+ let client = openedClients.get(dbUrl);
2335
+ if (!client) {
2336
+ client = new MongoClient(dbUrl, {
2337
+ connectTimeoutMS: definition.connectTimeoutMs ?? 1e4,
2338
+ serverSelectionTimeoutMS: definition.connectTimeoutMs ?? 1e4,
2339
+ ...definition.readPreference && { readPreference: definition.readPreference },
2340
+ ...definition.readConcern && { readConcern: definition.readConcern },
2341
+ ...definition.writeConcern && { writeConcern: definition.writeConcern }
2342
+ });
2343
+ client.on("error", () => {
2344
+ });
2345
+ openedClients.set(dbUrl, client);
2346
+ openedThisCall.push({ url: dbUrl, client });
2347
+ await client.connect();
2348
+ }
2349
+ const dbKey = `${key}Client`;
2350
+ db[dbKey] = buildManager2(key, definition.name, client);
2351
+ assignedThisCall.push(dbKey);
2352
+ }
2353
+ } catch (err) {
2354
+ await Promise.allSettled(openedThisCall.map(({ client }) => client.close()));
2355
+ for (const { url } of openedThisCall) openedClients.delete(url);
2356
+ for (const dbKey of assignedThisCall) delete db[dbKey];
2357
+ throw err;
2358
+ }
2359
+ };
2360
+ };
2361
+ var closeConnections = async (openedClients) => {
2362
+ const closes = [...openedClients.values()].map((client) => {
2363
+ try {
2364
+ return Promise.resolve(client.close()).catch(() => {
2365
+ });
2366
+ } catch {
2367
+ return Promise.resolve();
2368
+ }
2369
+ });
2370
+ await Promise.allSettled(closes);
2371
+ openedClients.clear();
2372
+ };
2373
+
2374
+ // src/service/health.ts
2375
+ var createHealth = (db) => async () => {
2376
+ const started = performance.now();
2377
+ const dbs = await Promise.all(
2378
+ Object.entries(db).map(async ([key, manager]) => {
2379
+ const name = key.replace(/Client$/, "");
2380
+ const handle = manager?.client;
2381
+ if (!handle) return { name, ok: false };
2382
+ try {
2383
+ const t0 = performance.now();
2384
+ await withTimeout(handle.admin().command({ ping: 1 }), 2e3, `health:${name}`);
2385
+ return { name, ok: true, latencyMs: performance.now() - t0 };
2386
+ } catch {
2387
+ return { name, ok: false };
2388
+ }
2389
+ })
2390
+ );
2391
+ return { ok: dbs.every((r) => r.ok), latencyMs: performance.now() - started, dbs };
2392
+ };
2393
+
2394
+ // src/relation/populate.ts
2395
+ var createForeignKeyLoader = (deps2, collection, foreignField, options = {}) => new DataLoader({
2396
+ maxBatchSize: options.maxBatchSize,
2397
+ cache: options.cache,
2398
+ keyOf: canonicalKey,
2399
+ batch: async (keys) => {
2400
+ const docs = await deps2.findMany(
2401
+ collection,
2402
+ { [foreignField]: { $in: keys } },
2403
+ {
2404
+ limit: options.maxDocs
2405
+ }
2406
+ );
2407
+ const index = /* @__PURE__ */ new Map();
2408
+ for (const doc of docs) {
2409
+ const value = doc[foreignField];
2410
+ const canonical = canonicalKey(value);
2411
+ const bucket = index.get(canonical) ?? [];
2412
+ bucket.push(doc);
2413
+ index.set(canonical, bucket);
2414
+ }
2415
+ const map = /* @__PURE__ */ new Map();
2416
+ for (const key of keys) {
2417
+ map.set(key, index.get(canonicalKey(key)) ?? []);
2418
+ }
2419
+ return map;
2420
+ }
2421
+ });
2422
+ var distinctKeys = (values) => {
2423
+ const seen = /* @__PURE__ */ new Set();
2424
+ const out = [];
2425
+ for (const value of values) {
2426
+ if (value === void 0 || value === null) continue;
2427
+ const canonical = canonicalKey(value);
2428
+ if (!seen.has(canonical)) {
2429
+ seen.add(canonical);
2430
+ out.push(value);
2431
+ }
2432
+ }
2433
+ return out;
2434
+ };
2435
+ var makePopulator = (deps2) => {
2436
+ const populate = async (docs, relations, options = {}) => {
2437
+ for (const relation of relations) {
2438
+ const target = relation.foreignField ?? "_id";
2439
+ const sourceKeys = distinctKeys(docs.map((doc) => doc[relation.localField]));
2440
+ if (relation.type === "belongsTo") {
2441
+ const loader = createForeignKeyLoader(deps2, relation.collection, target, options);
2442
+ const values = await loader.loadMany(sourceKeys);
2443
+ const byKey = new Map(sourceKeys.map((key, i) => [canonicalKey(key), values[i]]));
2444
+ for (const doc of docs) {
2445
+ const key = doc[relation.localField];
2446
+ const match = key === void 0 || key === null ? void 0 : byKey.get(canonicalKey(key))?.[0];
2447
+ doc[relation.as] = match ?? (relation.optional ? void 0 : null);
2448
+ }
2449
+ } else if (relation.type === "hasMany") {
2450
+ const loader = createForeignKeyLoader(deps2, relation.collection, target, options);
2451
+ const values = await loader.loadMany(sourceKeys);
2452
+ const byKey = new Map(sourceKeys.map((key, i) => [canonicalKey(key), values[i] ?? []]));
2453
+ for (const doc of docs) {
2454
+ const key = doc[relation.localField];
2455
+ doc[relation.as] = key === void 0 || key === null ? [] : byKey.get(canonicalKey(key)) ?? [];
2456
+ }
2457
+ } else {
2458
+ const throughLoader = createForeignKeyLoader(
2459
+ deps2,
2460
+ relation.through.collection,
2461
+ relation.through.localField,
2462
+ options
2463
+ );
2464
+ const throughValues = await throughLoader.loadMany(sourceKeys);
2465
+ const throughByKey = new Map(
2466
+ sourceKeys.map((key, i) => [canonicalKey(key), throughValues[i] ?? []])
2467
+ );
2468
+ const joinDocs = throughValues.flat();
2469
+ const targetKeys = distinctKeys(
2470
+ joinDocs.map((join3) => join3[relation.through.foreignField])
2471
+ );
2472
+ const targetLoader = createForeignKeyLoader(deps2, relation.collection, target, options);
2473
+ const targetValues = await targetLoader.loadMany(targetKeys);
2474
+ const targetByKey = new Map(
2475
+ targetKeys.map((key, i) => [canonicalKey(key), targetValues[i] ?? []])
2476
+ );
2477
+ for (const doc of docs) {
2478
+ const key = doc[relation.localField];
2479
+ const joins = key === void 0 || key === null ? [] : throughByKey.get(canonicalKey(key)) ?? [];
2480
+ const targets = joins.flatMap(
2481
+ (join3) => targetByKey.get(canonicalKey(join3[relation.through.foreignField])) ?? []
2482
+ );
2483
+ doc[relation.as] = targets;
2484
+ }
2485
+ }
2486
+ }
2487
+ return docs;
2488
+ };
2489
+ return { populate };
2490
+ };
2491
+
2492
+ // src/types.ts
2493
+ var asCollectionDefinition = (value) => {
2494
+ if (value && typeof value === "object" && "schema" in value) {
2495
+ const def = value;
2496
+ const ts = def.timestamps;
2497
+ const timestamps = ts === true ? { createdAt: "createdAt", updatedAt: "updatedAt" } : ts && typeof ts === "object" ? ts : void 0;
2498
+ return {
2499
+ schema: def.schema,
2500
+ indexes: def.indexes ?? [],
2501
+ hooks: def.hooks ?? {},
2502
+ ...timestamps ? { timestamps } : {}
2503
+ };
2504
+ }
2505
+ return { schema: value, indexes: [], hooks: {} };
2506
+ };
2507
+
2508
+ // src/service/trace-db-op.ts
2509
+ var traceDbOp = async (logger, meta, fn, options = {}) => {
2510
+ const start = performance.now();
2511
+ logger.debug({ ...meta, operation: "start" }, "start");
2512
+ try {
2513
+ const result = await fn();
2514
+ const durationMs = performance.now() - start;
2515
+ logger.info({ ...meta, durationMs }, "ok");
2516
+ return result;
2517
+ } catch (err) {
2518
+ const durationMs = performance.now() - start;
2519
+ const loggable = err instanceof Error ? { name: err.name, message: err.message, code: err.code } : err;
2520
+ logger.error({ ...meta, durationMs, error: loggable }, "error");
2521
+ if (options.wrapMongoErrors) throw mapMongoDriverError(err, meta);
2522
+ throw err;
2523
+ }
2524
+ };
2525
+
2526
+ // src/service/op-deps.ts
2527
+ var makeOpDeps = (logger, dbLabel, resolve, wrapMongoErrors) => {
2528
+ const trace = (meta2, fn) => traceDbOp(logger, meta2, fn, { wrapMongoErrors });
2529
+ const meta = (collection, op) => ({
2530
+ collection: String(collection),
2531
+ physicalCollection: resolve(String(collection)),
2532
+ db: dbLabel,
2533
+ op
2534
+ });
2535
+ return { trace, meta };
2536
+ };
2537
+
2538
+ // src/service/query-options.ts
2539
+ var resolveQueryOptions = (options) => {
2540
+ const maxAttempts = Math.max(1, options?.maxAttempts ?? 3);
2541
+ const retryDelayMs = options?.retryDelayMs ?? 150;
2542
+ const {
2543
+ session,
2544
+ maxTimeMS,
2545
+ hint,
2546
+ batchSize,
2547
+ drift,
2548
+ maxAttempts: _ma,
2549
+ retryDelayMs: _rd,
2550
+ dedupe,
2551
+ cache,
2552
+ retryWrites,
2553
+ ...rest
2554
+ } = options ?? {};
2555
+ return {
2556
+ maxAttempts,
2557
+ retryDelayMs,
2558
+ dedupe,
2559
+ cache,
2560
+ retryWrites,
2561
+ sdk: { session, maxTimeMS, hint, batchSize, drift },
2562
+ driverOpts: rest
2563
+ };
2564
+ };
2565
+ var resolveDriftMode = (perOp, serviceDefault) => {
2566
+ if (perOp === true) return "throw";
2567
+ if (perOp === false) return "off";
2568
+ if (perOp === "off" || perOp === "report" || perOp === "throw") return perOp;
2569
+ return serviceDefault ?? "report";
2570
+ };
2571
+
2572
+ // src/service/crud-op.ts
2573
+ var defineCrudOp = async (deps2, collection, opName, execute, options, write = false) => {
2574
+ const resolved = resolveQueryOptions(options);
2575
+ const maxAttempts = write && resolved.retryWrites !== true ? 1 : resolved.maxAttempts;
2576
+ return deps2.trace(
2577
+ deps2.meta(collection, opName),
2578
+ async () => withRetry(async () => execute(resolved), {
2579
+ maxAttempts,
2580
+ delayMs: resolved.retryDelayMs
2581
+ })
2582
+ );
2583
+ };
2584
+
2585
+ // src/service/pipeline-builder.ts
2586
+ var PipelineBuilder = class _PipelineBuilder {
2587
+ constructor(deps2, stages = []) {
2588
+ this.deps = deps2;
2589
+ this.stages = stages;
2590
+ }
2591
+ /** Raw accumulated stages — embeddable into `$lookup` / `$facet` branches. */
2592
+ raw() {
2593
+ return this.stages;
2594
+ }
2595
+ assertNonNegativeFinite(name, n) {
2596
+ if (!Number.isFinite(n) || n < 0) {
2597
+ throw new BadRequest(`pipeline.${name}(): ${name} must be a non-negative finite number`);
2598
+ }
2599
+ }
2600
+ /* --------------------------- shape-keeping --------------------------- */
2601
+ match(filter) {
2602
+ this.stages.push({ $match: filter });
2603
+ return this;
2604
+ }
2605
+ sort(sort) {
2606
+ this.stages.push({ $sort: sort });
2607
+ return this;
2608
+ }
2609
+ limit(n) {
2610
+ this.assertNonNegativeFinite("limit", n);
2611
+ this.stages.push({ $limit: n });
2612
+ return this;
2613
+ }
2614
+ skip(n) {
2615
+ this.assertNonNegativeFinite("skip", n);
2616
+ this.stages.push({ $skip: n });
2617
+ return this;
2618
+ }
2619
+ sample(n) {
2620
+ this.assertNonNegativeFinite("sample", n);
2621
+ this.stages.push({ $sample: { size: n } });
2622
+ return this;
2623
+ }
2624
+ /**
2625
+ * `$geoNear` — requires a `2dsphere`/`2d` index and MUST be the first stage.
2626
+ * Adds the computed distance to `spec.distanceField` on every output doc.
2627
+ */
2628
+ geoNear(spec) {
2629
+ if (this.stages.length > 0) {
2630
+ throw new Error("pipeline.geoNear(): $geoNear must be the first pipeline stage");
2631
+ }
2632
+ return this.derive({ $geoNear: spec });
2633
+ }
2634
+ /* --------------------------- shape-changing -------------------------- */
2635
+ project(projection) {
2636
+ return this.derive({ $project: projection });
2637
+ }
2638
+ addFields(fields) {
2639
+ return this.derive({ $addFields: fields });
2640
+ }
2641
+ set(fields) {
2642
+ return this.derive({ $set: fields });
2643
+ }
2644
+ unset(fields) {
2645
+ return this.derive({ $unset: fields });
2646
+ }
2647
+ count(field) {
2648
+ return this.derive({ $count: field });
2649
+ }
2650
+ group(group) {
2651
+ return this.derive({ $group: group });
2652
+ }
2653
+ unwind(path) {
2654
+ return this.derive({ $unwind: path });
2655
+ }
2656
+ lookup(spec) {
2657
+ return this.derive({
2658
+ $lookup: this.evalLookup(spec)
2659
+ });
2660
+ }
2661
+ facet(facets) {
2662
+ return this.derive({ $facet: this.evalFacets(facets) });
2663
+ }
2664
+ /* ------------------------------ terminals ---------------------------- */
2665
+ /** Run the pipeline and return every output document. */
2666
+ toArray() {
2667
+ return defineCrudOp(
2668
+ this.deps.crudDeps,
2669
+ this.deps.logical,
2670
+ "mongo.pipeline",
2671
+ (r) => this.deps.coll().aggregate(this.stages, this.deps.mergeDriver(r.driverOpts, r.sdk)).toArray(),
2672
+ this.deps.options
2673
+ );
2674
+ }
2675
+ /** Run the pipeline and return the first output document (or null). */
2676
+ async first() {
2677
+ return defineCrudOp(
2678
+ this.deps.crudDeps,
2679
+ this.deps.logical,
2680
+ "mongo.pipeline",
2681
+ async (r) => {
2682
+ const cursor = this.deps.coll().aggregate(this.stages, this.deps.mergeDriver(r.driverOpts, r.sdk));
2683
+ try {
2684
+ return await cursor.tryNext();
2685
+ } finally {
2686
+ await cursor.close();
2687
+ }
2688
+ },
2689
+ this.deps.options
2690
+ );
2691
+ }
2692
+ /** Streaming cursor (raw driver cursor; no retry / trace). */
2693
+ cursor() {
2694
+ return this.deps.coll().aggregate(this.stages, this.deps.options);
2695
+ }
2696
+ /* ------------------------------ internals ---------------------------- */
2697
+ derive(stage) {
2698
+ return new _PipelineBuilder(this.deps, [...this.stages, stage]);
2699
+ }
2700
+ evalLookup(spec) {
2701
+ const { pipeline, ...rest } = spec;
2702
+ if (typeof pipeline === "function") {
2703
+ const sub = new _PipelineBuilder(this.deps, []);
2704
+ const result = pipeline(sub);
2705
+ return { ...rest, pipeline: normalizeSubPipeline(result, "$lookup") };
2706
+ }
2707
+ return spec;
2708
+ }
2709
+ evalFacets(facets) {
2710
+ const out = {};
2711
+ for (const [name, branch] of Object.entries(facets)) {
2712
+ if (typeof branch === "function") {
2713
+ const sub = new _PipelineBuilder(this.deps, []);
2714
+ const result = branch(sub);
2715
+ out[name] = normalizeSubPipeline(result, `$facet.${name}`);
2716
+ } else {
2717
+ out[name] = branch;
2718
+ }
2719
+ }
2720
+ return out;
2721
+ }
2722
+ };
2723
+ var normalizeSubPipeline = (result, where) => {
2724
+ if (Array.isArray(result) && result.some((s2) => s2 instanceof PipelineBuilder)) {
2725
+ throw new TypeError(
2726
+ `aggregation sub-pipeline (${where}) must be written as a chained builder \u2014 e.g. \`(o) => o.match(...).project(...)\` \u2014 not as an array of builder results like \`[o.match(...), o.project(...)]\` (each element is a builder, not a stage).`
2727
+ );
2728
+ }
2729
+ return result instanceof PipelineBuilder ? result.raw() : result;
2730
+ };
2731
+ var stageBuilder = () => new PipelineBuilder({
2732
+ logical: "",
2733
+ coll: () => {
2734
+ throw new Error("stage-only pipeline builders cannot be executed");
2735
+ },
2736
+ crudDeps: {
2737
+ trace: () => Promise.reject(new Error("stage-only pipeline builders cannot be executed")),
2738
+ meta: () => ({ collection: "", physicalCollection: "", db: "", op: "" })
2739
+ },
2740
+ mergeDriver: () => ({})
2741
+ });
2742
+
2743
+ // src/service/aggregation-stages.ts
2744
+ var buildAggregationStages = () => {
2745
+ let emitted = 0;
2746
+ const mark = (stage) => {
2747
+ emitted++;
2748
+ return stage;
2749
+ };
2750
+ return {
2751
+ match: (filter) => mark({ $match: filter }),
2752
+ project: (fields) => mark({ $project: fields }),
2753
+ addFields: (fields) => mark({ $addFields: fields }),
2754
+ set: (fields) => mark({ $set: fields }),
2755
+ unset: (fields) => mark({ $unset: fields }),
2756
+ sort: (sort) => mark({ $sort: sort }),
2757
+ limit: (n) => mark({ $limit: n }),
2758
+ skip: (n) => mark({ $skip: n }),
2759
+ count: (field) => mark({ $count: field }),
2760
+ group: (group) => mark({ $group: group }),
2761
+ lookup: (params) => {
2762
+ const { pipeline, ...rest } = params;
2763
+ if (typeof pipeline === "function") {
2764
+ const sub = stageBuilder();
2765
+ const result = pipeline(sub);
2766
+ return mark({
2767
+ $lookup: {
2768
+ ...rest,
2769
+ pipeline: normalizeSubPipeline(result, "$lookup")
2770
+ }
2771
+ });
2772
+ }
2773
+ return mark({ $lookup: params });
2774
+ },
2775
+ facet: (facets) => {
2776
+ const out = {};
2777
+ for (const [name, branch] of Object.entries(facets)) {
2778
+ out[name] = typeof branch === "function" ? branch(
2779
+ buildAggregationStages()
2780
+ ) : branch;
2781
+ }
2782
+ return mark({ $facet: out });
2783
+ },
2784
+ unwind: (path) => mark({ $unwind: path }),
2785
+ fill: (params) => mark({ $fill: params }),
2786
+ bucket: (params) => mark({ $bucket: params }),
2787
+ bucketAuto: (params) => mark({ $bucketAuto: params }),
2788
+ densify: (params) => mark({ $densify: params }),
2789
+ geoNear: (spec) => {
2790
+ if (emitted > 0) {
2791
+ throw new Error("aggregate.geoNear(): $geoNear must be the first pipeline stage");
2792
+ }
2793
+ return mark({ $geoNear: spec });
2794
+ }
2795
+ };
2796
+ };
2797
+
2798
+ // src/service/aggregation/aggregate.ts
2799
+ var makeAggregateOps = (ctx) => {
2800
+ const { deps: deps2, coll, mergeAggOptions: mergeAggOptions2 } = ctx;
2801
+ const aggregate = async (collection, pipelineBuilder, options) => {
2802
+ const stages = buildAggregationStages();
2803
+ const pipeline2 = pipelineBuilder(stages);
2804
+ return defineCrudOp(
2805
+ deps2,
2806
+ collection,
2807
+ "mongo.aggregate",
2808
+ (r) => coll(collection).aggregate(pipeline2, mergeAggOptions2(r.driverOpts, r.sdk)),
2809
+ options
2810
+ );
2811
+ };
2812
+ const pipeline = (collection, options) => new PipelineBuilder({
2813
+ logical: collection,
2814
+ coll: () => coll(collection),
2815
+ crudDeps: deps2,
2816
+ mergeDriver: mergeAggOptions2,
2817
+ options
2818
+ });
2819
+ return { aggregate, pipeline };
2820
+ };
2821
+
2822
+ // src/service/aggregation/helpers.ts
2823
+ var mergeAggOptions = (driverOpts, sdk) => ({
2824
+ ...driverOpts,
2825
+ session: sdk.session,
2826
+ maxTimeMS: sdk.maxTimeMS,
2827
+ hint: sdk.hint,
2828
+ batchSize: sdk.batchSize
2829
+ });
2830
+ var DATE_PART_FORMATS = {
2831
+ hour: "%Y-%m-%dT%H",
2832
+ day: "%Y-%m-%d",
2833
+ week: "%Y-W%V",
2834
+ month: "%Y-%m",
2835
+ year: "%Y"
2836
+ };
2837
+
2838
+ // src/service/aggregation/group.ts
2839
+ var makeGroupOps = (ctx) => {
2840
+ const { deps: deps2, coll, mergeAggOptions: mergeAggOptions2 } = ctx;
2841
+ const groupBy = async (collection, baseFilter, config, customization = {}, options) => {
2842
+ if (config.limit !== void 0 && (!Number.isFinite(config.limit) || config.limit < 0)) {
2843
+ throw new BadRequest("groupBy: `limit` must be a non-negative finite number");
2844
+ }
2845
+ const pipeline = [
2846
+ ...customization.prePipeline ?? [],
2847
+ { $match: baseFilter },
2848
+ { $group: { _id: config.groupBy, ...config.accumulate ?? {} } },
2849
+ ...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
2850
+ ...config.limit !== void 0 ? [{ $limit: config.limit }] : [],
2851
+ ...customization.postPipeline ?? []
2852
+ ];
2853
+ return defineCrudOp(
2854
+ deps2,
2855
+ collection,
2856
+ "mongo.groupBy",
2857
+ (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray(),
2858
+ options
2859
+ );
2860
+ };
2861
+ const dateRangeAnalysis = async (collection, baseFilter, dateConfig, analysisConfig = {}, customization = {}, options) => {
2862
+ if (analysisConfig.limit !== void 0 && (!Number.isFinite(analysisConfig.limit) || analysisConfig.limit < 0)) {
2863
+ throw new BadRequest("dateRangeAnalysis: `limit` must be a non-negative finite number");
2864
+ }
2865
+ const granularity = dateConfig.granularity ?? "day";
2866
+ const format = DATE_PART_FORMATS[granularity];
2867
+ const datePart = {
2868
+ $dateToString: { format, date: `$${dateConfig.dateField}` }
2869
+ };
2870
+ const pipeline = [
2871
+ ...customization.prePipeline ?? [],
2872
+ {
2873
+ $match: {
2874
+ ...baseFilter,
2875
+ [dateConfig.dateField]: { $gte: dateConfig.startDate, $lte: dateConfig.endDate }
2876
+ }
2877
+ },
2878
+ { $group: { _id: datePart, ...analysisConfig.accumulate ?? {} } },
2879
+ ...analysisConfig.sort && Object.keys(analysisConfig.sort).length > 0 ? [{ $sort: analysisConfig.sort }] : [],
2880
+ ...analysisConfig.limit !== void 0 ? [{ $limit: analysisConfig.limit }] : [],
2881
+ ...customization.postPipeline ?? []
2882
+ ];
2883
+ return defineCrudOp(
2884
+ deps2,
2885
+ collection,
2886
+ "mongo.dateRangeAnalysis",
2887
+ (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray(),
2888
+ options
2889
+ );
2890
+ };
2891
+ return { groupBy, dateRangeAnalysis };
2892
+ };
2893
+
2894
+ // src/service/aggregation/lookup-join.ts
2895
+ var makeLookupJoinOp = (ctx) => {
2896
+ const { deps: deps2, coll, mergeAggOptions: mergeAggOptions2, resolve } = ctx;
2897
+ const lookupJoin = async (collection, baseFilter, lookups, customization = {}, options) => {
2898
+ const pipeline = [
2899
+ ...customization.prePipeline ?? [],
2900
+ { $match: baseFilter },
2901
+ ...lookups.map((lookup) => ({
2902
+ $lookup: {
2903
+ from: resolve(lookup.fromCollection),
2904
+ localField: lookup.localField,
2905
+ foreignField: lookup.foreignField,
2906
+ as: lookup.as,
2907
+ ...lookup.pipeline ? { pipeline: lookup.pipeline } : {}
2908
+ }
2909
+ })),
2910
+ ...lookups.filter((lookup) => lookup.unwindSingle).map((lookup) => ({
2911
+ $unwind: { path: `$${lookup.as}`, preserveNullAndEmptyArrays: true }
2912
+ })),
2913
+ ...customization.postPipeline ?? []
2914
+ ];
2915
+ return defineCrudOp(
2916
+ deps2,
2917
+ collection,
2918
+ "mongo.lookupJoin",
2919
+ (r) => coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray(),
2920
+ options
2921
+ );
2922
+ };
2923
+ return { lookupJoin };
2924
+ };
2925
+
2926
+ // src/shared/pagination-math.ts
2927
+ var normalizePageLimit = (query, maxLimit, defaults = {}) => {
2928
+ const rawPage = query.page ?? defaults.page ?? 1;
2929
+ const limit = query.limit ?? defaults.limit ?? 10;
2930
+ if (typeof rawPage !== "number" || !Number.isFinite(rawPage)) {
2931
+ throw new BadRequest("page must be a finite number");
2932
+ }
2933
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
2934
+ throw new BadRequest("limit must be a finite number");
2935
+ }
2936
+ if (limit < 1) throw new BadRequest("limit must be >= 1");
2937
+ if (limit > maxLimit) throw new BadRequest(`limit ${limit} exceeds maxLimit ${maxLimit}`);
2938
+ const page = Math.max(1, Math.floor(rawPage));
2939
+ return { page, limit, offset: (page - 1) * limit };
2940
+ };
2941
+
2942
+ // src/shared/pagination-result.ts
2943
+ var buildPaginationResult = (data, totalCount, page, limit) => {
2944
+ const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / limit);
2945
+ return {
2946
+ data,
2947
+ totalCount,
2948
+ hasNextPage: page < totalPages,
2949
+ hasPrevPage: page > 1,
2950
+ currentPage: page,
2951
+ totalPages
2952
+ };
2953
+ };
2954
+
2955
+ // src/service/aggregation-pipeline.ts
2956
+ var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2957
+ var buildRegexSearchStages = (baseFilter, searchConfig) => {
2958
+ const { searchFields, searchTerm, fuzzy = false, caseSensitive = false } = searchConfig;
2959
+ const escaped = escapeRegex(searchTerm);
2960
+ const pattern = fuzzy ? escaped.split("").join(".*") : escaped;
2961
+ const $or = searchFields.map((field) => ({
2962
+ [field]: { $regex: pattern, $options: caseSensitive ? "" : "i" }
2963
+ }));
2964
+ return [{ $match: { ...baseFilter, $or } }];
2965
+ };
2966
+ var buildTextSearchStages = (baseFilter, searchConfig) => [
2967
+ {
2968
+ $match: {
2969
+ ...baseFilter,
2970
+ $text: {
2971
+ $search: searchConfig.searchTerm,
2972
+ ...searchConfig.caseSensitive !== void 0 && {
2973
+ $caseSensitive: searchConfig.caseSensitive
2974
+ },
2975
+ ...searchConfig.language !== void 0 && { $language: searchConfig.language },
2976
+ ...searchConfig.diacriticSensitive !== void 0 && {
2977
+ $diacriticSensitive: searchConfig.diacriticSensitive
2978
+ }
2979
+ }
2980
+ }
2981
+ },
2982
+ { $addFields: { searchScore: { $meta: "textScore" } } }
2983
+ ];
2984
+ var buildSearchStages = (baseFilter, searchConfig) => searchConfig.useRegex ? buildRegexSearchStages(baseFilter, searchConfig) : buildTextSearchStages(baseFilter, searchConfig);
2985
+
2986
+ // src/service/aggregation/text-search.ts
2987
+ var makeTextSearchOp = (ctx) => {
2988
+ const { deps: deps2, coll, mergeAggOptions: mergeAggOptions2 } = ctx;
2989
+ const textSearch = async (collection, baseFilter, searchConfig, paginationConfig = {}, customization = {}, options) => {
2990
+ const normalized = normalizePageLimit(
2991
+ { page: paginationConfig.page, limit: paginationConfig.limit },
2992
+ paginationConfig.maxLimit ?? DEFAULT_MAX_LIMIT
2993
+ );
2994
+ const searchStages = buildSearchStages(
2995
+ baseFilter,
2996
+ searchConfig
2997
+ );
2998
+ const pipeline = [
2999
+ ...customization.prePipeline ?? [],
3000
+ ...searchStages,
3001
+ {
3002
+ $facet: {
3003
+ data: [
3004
+ ...searchConfig.sortByScore ? [{ $sort: { searchScore: { $meta: "textScore" } } }] : [],
3005
+ ...paginationConfig.sort && Object.keys(paginationConfig.sort).length > 0 ? [{ $sort: paginationConfig.sort }] : [],
3006
+ { $skip: normalized.offset },
3007
+ { $limit: normalized.limit },
3008
+ ...customization.postPipeline ?? []
3009
+ ],
3010
+ totalCount: [{ $count: "count" }]
3011
+ }
3012
+ }
3013
+ ];
3014
+ return defineCrudOp(
3015
+ deps2,
3016
+ collection,
3017
+ "mongo.textSearch",
3018
+ async (r) => {
3019
+ const rows = await coll(collection).aggregate(pipeline, mergeAggOptions2(r.driverOpts, r.sdk)).toArray();
3020
+ const row = rows[0];
3021
+ const data = row?.data ?? [];
3022
+ const totalCount = row?.totalCount?.[0]?.count ?? 0;
3023
+ return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
3024
+ },
3025
+ options
3026
+ );
3027
+ };
3028
+ return { textSearch };
3029
+ };
3030
+
3031
+ // src/service/aggregation/index.ts
3032
+ var makeAggregationOps = (client, dbLabel, logger, opts) => {
3033
+ const resolve = opts.resolveCollectionName;
3034
+ const deps2 = makeOpDeps(logger, dbLabel, resolve, opts.wrapMongoErrors === true);
3035
+ const coll = (collection) => client.collection(resolve(String(collection)));
3036
+ const ctx = {
3037
+ resolve,
3038
+ deps: deps2,
3039
+ coll,
3040
+ mergeAggOptions
3041
+ };
3042
+ return {
3043
+ ...makeAggregateOps(ctx),
3044
+ ...makeGroupOps(ctx),
3045
+ ...makeTextSearchOp(ctx),
3046
+ ...makeLookupJoinOp(ctx)
3047
+ };
3048
+ };
3049
+
3050
+ // src/service/drift.ts
3051
+ var checkDocsDrift = (options, collection, op, docs) => {
3052
+ if (options.drift === "off") return;
3053
+ const schema = options.getSchema?.(collection);
3054
+ if (!schema) return;
3055
+ for (const doc of docs) {
3056
+ const issues = validateDoc(schema, doc);
3057
+ if (issues.length === 0) continue;
3058
+ const detail = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
3059
+ const extra = { collection, db: options.db, op, issues };
3060
+ if (options.drift === "throw") {
3061
+ throw new DomainError(
3062
+ "SCHEMA_DRIFT",
3063
+ `Document drifted from schema in "${collection}": ${detail}`,
3064
+ extra
3065
+ );
3066
+ }
3067
+ options.logger.warn(extra, `[drift] ${collection} (${options.db}, ${op}): ${detail}`);
3068
+ }
3069
+ };
3070
+
3071
+ // src/service/update-format.ts
3072
+ var hasUpdateOperator = (update) => {
3073
+ if (!update) return false;
3074
+ return Object.keys(update).some((key) => key.startsWith("$"));
3075
+ };
3076
+ var formatUpdateFilter = (update) => {
3077
+ if (hasUpdateOperator(update)) {
3078
+ return update;
3079
+ }
3080
+ return { $set: update };
3081
+ };
3082
+
3083
+ // src/service/crud/context.ts
3084
+ var createCrudContext = (client, dbLabel, logger, opts) => {
3085
+ const resolve = opts.resolveCollectionName;
3086
+ const deps2 = makeOpDeps(
3087
+ logger,
3088
+ dbLabel,
3089
+ resolve,
3090
+ opts.wrapMongoErrors === true
3091
+ );
3092
+ const coll = (name) => client.collection(resolve(String(name)));
3093
+ const toDriverFilter = (filter) => filter;
3094
+ const run = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options);
3095
+ const writeRun = (collection, opName, execute, options) => defineCrudOp(deps2, collection, opName, execute, options, true);
3096
+ const invalidate = (collection) => {
3097
+ opts.cache?.invalidateByCollection(
3098
+ cacheCollectionKey(client.databaseName, resolve(String(collection)))
3099
+ );
3100
+ };
3101
+ const timestampsFor = (collection) => opts.timestamps?.[String(collection)];
3102
+ const stampCreate = (collection, doc) => {
3103
+ const ts = timestampsFor(collection);
3104
+ if (!ts) return;
3105
+ const now = /* @__PURE__ */ new Date();
3106
+ if (ts.createdAt && doc[ts.createdAt] === void 0) doc[ts.createdAt] = now;
3107
+ if (ts.updatedAt && doc[ts.updatedAt] === void 0) doc[ts.updatedAt] = now;
3108
+ };
3109
+ const stampUpdate = (collection, update) => {
3110
+ const formatted = formatUpdateFilter(update);
3111
+ const ts = timestampsFor(collection);
3112
+ if (ts?.updatedAt) {
3113
+ const $set = { ...formatted.$set, [ts.updatedAt]: /* @__PURE__ */ new Date() };
3114
+ return { ...formatted, $set };
3115
+ }
3116
+ return formatted;
3117
+ };
3118
+ const stampReplace = (collection, doc) => {
3119
+ const ts = timestampsFor(collection);
3120
+ if (!ts?.updatedAt) return doc;
3121
+ return { ...doc, [ts.updatedAt]: /* @__PURE__ */ new Date() };
3122
+ };
3123
+ const read = async (collection, opName, filter, options, execute, shape = "none") => {
3124
+ const physical = resolve(String(collection));
3125
+ const resolved = resolveQueryOptions(options);
3126
+ const noSession = !resolved.sdk.session;
3127
+ const useCache = opts.cache !== void 0 && noSession && resolved.cache !== false;
3128
+ const shouldDedupe = resolved.dedupe !== false && (resolved.dedupe === true || opts.dedupeReads === true && noSession) && opts.inFlight !== void 0;
3129
+ const checkDrift = (value) => {
3130
+ const mode = resolveDriftMode(resolved.sdk.drift, opts.drift);
3131
+ if (mode === "off") return;
3132
+ if (resolved.driverOpts.projection !== void 0) return;
3133
+ const docs = shape === "many" ? value : shape === "one" && value !== null ? [value] : [];
3134
+ checkDocsDrift(
3135
+ { logger, db: dbLabel, drift: mode, getSchema: opts.getSchema },
3136
+ String(collection),
3137
+ opName,
3138
+ docs
3139
+ );
3140
+ };
3141
+ const executeWrapped = () => deps2.trace(
3142
+ deps2.meta(collection, opName),
3143
+ () => withRetry(() => execute(resolved), {
3144
+ maxAttempts: resolved.maxAttempts,
3145
+ delayMs: resolved.retryDelayMs
3146
+ })
3147
+ );
3148
+ const key = useCache ? opts.cache.key(
3149
+ cacheCollectionKey(client.databaseName, physical),
3150
+ stableHash([filter, options])
3151
+ ) : void 0;
3152
+ const fetchAndCheck = () => executeWrapped().then((value) => {
3153
+ checkDrift(value);
3154
+ return value;
3155
+ });
3156
+ const runOnce = () => {
3157
+ if (useCache && key) {
3158
+ const hit = opts.cache.get(key);
3159
+ if (hit !== void 0) return Promise.resolve(hit);
3160
+ return fetchAndCheck().then((value) => {
3161
+ opts.cache.set(key, value);
3162
+ return value;
3163
+ });
3164
+ }
3165
+ return fetchAndCheck();
3166
+ };
3167
+ if (shouldDedupe) {
3168
+ const dedupeKey = `${cacheCollectionKey(client.databaseName, physical)}|${opName}|${stableHash([filter, options])}`;
3169
+ return opts.inFlight.run(dedupeKey, runOnce);
3170
+ }
3171
+ return runOnce();
3172
+ };
3173
+ const normalizeFindOptions = (options) => {
3174
+ if (!options) return {};
3175
+ const { select, ...rest } = options;
3176
+ if (!select || select.length === 0) return rest;
3177
+ const projection = {};
3178
+ for (const field of select) projection[String(field)] = 1;
3179
+ return { ...rest, projection };
3180
+ };
3181
+ return {
3182
+ client,
3183
+ dbLabel,
3184
+ logger,
3185
+ opts,
3186
+ resolve,
3187
+ trace: deps2.trace,
3188
+ meta: deps2.meta,
3189
+ coll,
3190
+ toDriverFilter,
3191
+ run,
3192
+ writeRun,
3193
+ invalidate,
3194
+ stampCreate,
3195
+ stampUpdate,
3196
+ stampReplace,
3197
+ read,
3198
+ normalizeFindOptions
3199
+ };
3200
+ };
3201
+
3202
+ // src/shared/soft-delete.ts
3203
+ var buildMongoActiveFilter = () => ({ $or: [{ deletedAt: { $exists: false } }, { deletedAt: null }] });
3204
+ var mergeMongoActiveFilter = (softDelete, filter) => {
3205
+ if (!softDelete) return filter ?? {};
3206
+ const active = buildMongoActiveFilter();
3207
+ if (!filter || Object.keys(filter).length === 0) return active;
3208
+ return { $and: [filter, active] };
3209
+ };
3210
+
3211
+ // src/service/crud/read-ops.ts
3212
+ var makeReadOps = (ctx) => {
3213
+ const { client, opts } = ctx;
3214
+ const { coll, toDriverFilter, read, normalizeFindOptions } = ctx;
3215
+ const getOne = async (collection, filter, options) => {
3216
+ const f = filter ?? {};
3217
+ const o = normalizeFindOptions(options);
3218
+ const doc = await read(
3219
+ collection,
3220
+ "mongo.getOne",
3221
+ f,
3222
+ o,
3223
+ async (r) => {
3224
+ const opts2 = {
3225
+ ...r.driverOpts,
3226
+ session: r.sdk.session,
3227
+ maxTimeMS: r.sdk.maxTimeMS,
3228
+ hint: r.sdk.hint
3229
+ };
3230
+ return coll(collection).findOne(toDriverFilter(f), opts2);
3231
+ },
3232
+ "one"
3233
+ );
3234
+ if (doc !== null && hasHook(opts.hooks, String(collection), "afterRead")) {
3235
+ const exposed = cloneDeep(doc);
3236
+ await runHooks(opts.hooks, String(collection), "afterRead", {
3237
+ collection: String(collection),
3238
+ doc: exposed
3239
+ });
3240
+ return exposed;
3241
+ }
3242
+ return doc;
3243
+ };
3244
+ const getOneOrFail = async (collection, filter, options, errorFactory) => {
3245
+ const doc = await getOne(collection, filter, options);
3246
+ if (doc === null) {
3247
+ if (errorFactory) throw errorFactory(String(collection));
3248
+ throw new DomainError(
3249
+ "NOT_FOUND",
3250
+ `Document not found in collection "${String(collection)}"`,
3251
+ {
3252
+ collection: String(collection)
3253
+ }
3254
+ );
3255
+ }
3256
+ return doc;
3257
+ };
3258
+ const findMany = async (collection, filter, options) => {
3259
+ const merged = normalizeFindOptions({
3260
+ ...options,
3261
+ limit: options?.limit ?? DEFAULT_FIND_LIMIT
3262
+ });
3263
+ const docs = await read(
3264
+ collection,
3265
+ "mongo.findMany",
3266
+ filter ?? {},
3267
+ merged,
3268
+ async (r) => {
3269
+ const opts2 = {
3270
+ ...r.driverOpts,
3271
+ session: r.sdk.session,
3272
+ maxTimeMS: r.sdk.maxTimeMS,
3273
+ hint: r.sdk.hint
3274
+ };
3275
+ return coll(collection).find(toDriverFilter(filter ?? {}), opts2).toArray();
3276
+ },
3277
+ "many"
3278
+ );
3279
+ if (hasHook(opts.hooks, String(collection), "afterRead")) {
3280
+ const exposed = cloneDeep(docs);
3281
+ await runHooks(opts.hooks, String(collection), "afterRead", {
3282
+ collection: String(collection),
3283
+ docs: exposed
3284
+ });
3285
+ return exposed;
3286
+ }
3287
+ return docs;
3288
+ };
3289
+ const findManyCursor = (collection, filter, options) => {
3290
+ const {
3291
+ session,
3292
+ hint,
3293
+ maxTimeMS,
3294
+ batchSize,
3295
+ maxAttempts: _ma,
3296
+ retryDelayMs: _rd,
3297
+ dedupe: _d,
3298
+ cache: _c,
3299
+ ...rest
3300
+ } = normalizeFindOptions(options);
3301
+ const findOpts = {
3302
+ ...rest,
3303
+ ...session && { session },
3304
+ ...hint && { hint },
3305
+ ...maxTimeMS && { maxTimeMS },
3306
+ ...batchSize && { batchSize }
3307
+ };
3308
+ return coll(collection).find(toDriverFilter(filter ?? {}), findOpts);
3309
+ };
3310
+ const findActive = async (collection, filter, options) => findMany(collection, mergeMongoActiveFilter(true, filter), options);
3311
+ const findActiveOne = async (collection, filter, options) => getOne(collection, mergeMongoActiveFilter(true, filter), options);
3312
+ const countDocuments = async (collection, filter, options) => read(
3313
+ collection,
3314
+ "mongo.countDocuments",
3315
+ filter ?? {},
3316
+ options,
3317
+ async (r) => {
3318
+ const opts2 = {
3319
+ ...r.driverOpts,
3320
+ session: r.sdk.session,
3321
+ maxTimeMS: r.sdk.maxTimeMS,
3322
+ hint: r.sdk.hint
3323
+ };
3324
+ return coll(collection).countDocuments(toDriverFilter(filter ?? {}), opts2);
3325
+ },
3326
+ "none"
3327
+ );
3328
+ const distinct = async (collection, field, filter, options) => {
3329
+ const f = filter ?? {};
3330
+ const o = normalizeFindOptions(options);
3331
+ return read(
3332
+ collection,
3333
+ "mongo.distinct",
3334
+ f,
3335
+ o,
3336
+ async (r) => {
3337
+ const opts2 = {
3338
+ ...r.driverOpts,
3339
+ session: r.sdk.session,
3340
+ maxTimeMS: r.sdk.maxTimeMS,
3341
+ hint: r.sdk.hint
3342
+ };
3343
+ return coll(collection).distinct(
3344
+ String(field),
3345
+ toDriverFilter(f),
3346
+ opts2
3347
+ );
3348
+ },
3349
+ "none"
3350
+ );
3351
+ };
3352
+ const estimatedDocumentCount = async (collection, options) => ctx.run(
3353
+ collection,
3354
+ "mongo.estimatedDocumentCount",
3355
+ async (r) => {
3356
+ const opts2 = {
3357
+ ...r.driverOpts,
3358
+ session: r.sdk.session,
3359
+ maxTimeMS: r.sdk.maxTimeMS
3360
+ };
3361
+ return coll(collection).estimatedDocumentCount(opts2);
3362
+ },
3363
+ options
3364
+ );
3365
+ return {
3366
+ getOne,
3367
+ getOneOrFail,
3368
+ findMany,
3369
+ findManyCursor,
3370
+ findActive,
3371
+ findActiveOne,
3372
+ countDocuments,
3373
+ distinct,
3374
+ estimatedDocumentCount
3375
+ };
3376
+ };
3377
+
3378
+ // src/service/crud/watch-ops.ts
3379
+ var makeWatchOps = (ctx) => {
3380
+ const { client, resolve, coll, read } = ctx;
3381
+ const watchCollection = (collection, pipeline, options) => coll(collection).watch(pipeline, options);
3382
+ const query = (collection) => {
3383
+ const physical = resolve(String(collection));
3384
+ const handle = client.collection(physical);
3385
+ return new QueryBuilder({
3386
+ physicalName: physical,
3387
+ collection: handle,
3388
+ run: async (opName, filter, execute, options) => read(
3389
+ collection,
3390
+ opName,
3391
+ filter,
3392
+ options,
3393
+ async (r) => {
3394
+ const opts = {
3395
+ ...r.driverOpts,
3396
+ session: r.sdk.session,
3397
+ maxTimeMS: r.sdk.maxTimeMS,
3398
+ hint: r.sdk.hint
3399
+ };
3400
+ return execute(opts);
3401
+ },
3402
+ opName === "mongo.getOne" ? "one" : opName === "mongo.findMany" ? "many" : "none"
3403
+ )
3404
+ });
3405
+ };
3406
+ return {
3407
+ watchCollection,
3408
+ query
3409
+ };
3410
+ };
3411
+
3412
+ // src/service/crud/write-ops.ts
3413
+ var makeWriteOps = (ctx, readOps) => {
3414
+ const { opts } = ctx;
3415
+ const { coll, toDriverFilter, writeRun, invalidate, stampCreate, stampUpdate, stampReplace } = ctx;
3416
+ const { getOne } = readOps;
3417
+ const insertOne = async (collection, doc, options) => {
3418
+ await runHooks(opts.hooks, String(collection), "beforeCreate", {
3419
+ collection: String(collection),
3420
+ doc
3421
+ });
3422
+ stampCreate(collection, doc);
3423
+ const result = await writeRun(
3424
+ collection,
3425
+ "mongo.insertOne",
3426
+ async (r) => {
3427
+ const insertOpts = {
3428
+ ...r.driverOpts,
3429
+ session: r.sdk.session
3430
+ };
3431
+ return coll(collection).insertOne(doc, insertOpts);
3432
+ },
3433
+ options
3434
+ );
3435
+ invalidate(collection);
3436
+ await runHooks(opts.hooks, String(collection), "afterCreate", {
3437
+ collection: String(collection),
3438
+ doc
3439
+ });
3440
+ return result;
3441
+ };
3442
+ const insertMany = async (collection, docs, options) => {
3443
+ await runHooks(opts.hooks, String(collection), "beforeCreate", {
3444
+ collection: String(collection),
3445
+ docs
3446
+ });
3447
+ for (const d of docs) stampCreate(collection, d);
3448
+ if (docs.length > MAX_BATCH_OPS) {
3449
+ throw new BadRequest(
3450
+ `insertMany: too many docs (${docs.length} > ${MAX_BATCH_OPS}); split the batch`
3451
+ );
3452
+ }
3453
+ const result = await writeRun(
3454
+ collection,
3455
+ "mongo.insertMany",
3456
+ async (r) => {
3457
+ const bulkOpts = {
3458
+ ...r.driverOpts,
3459
+ session: r.sdk.session
3460
+ };
3461
+ return coll(collection).insertMany(
3462
+ docs,
3463
+ bulkOpts
3464
+ );
3465
+ },
3466
+ options
3467
+ );
3468
+ invalidate(collection);
3469
+ await runHooks(opts.hooks, String(collection), "afterCreate", {
3470
+ collection: String(collection),
3471
+ docs
3472
+ });
3473
+ return result;
3474
+ };
3475
+ const updateOne = async (collection, filter, update, options) => {
3476
+ await runHooks(opts.hooks, String(collection), "beforeUpdate", {
3477
+ collection: String(collection),
3478
+ filter
3479
+ });
3480
+ const effective = stampUpdate(collection, update);
3481
+ const result = await writeRun(
3482
+ collection,
3483
+ "mongo.updateOne",
3484
+ async (r) => {
3485
+ const updateOpts = {
3486
+ ...r.driverOpts,
3487
+ session: r.sdk.session
3488
+ };
3489
+ return coll(collection).updateOne(
3490
+ toDriverFilter(filter),
3491
+ effective,
3492
+ updateOpts
3493
+ );
3494
+ },
3495
+ options
3496
+ );
3497
+ invalidate(collection);
3498
+ await runHooks(opts.hooks, String(collection), "afterUpdate", {
3499
+ collection: String(collection),
3500
+ filter
3501
+ });
3502
+ return result;
3503
+ };
3504
+ const updateMany = async (collection, filter, update, options) => {
3505
+ const effective = stampUpdate(collection, update);
3506
+ const result = await writeRun(
3507
+ collection,
3508
+ "mongo.updateMany",
3509
+ async (r) => {
3510
+ const updateOpts = {
3511
+ ...r.driverOpts,
3512
+ session: r.sdk.session
3513
+ };
3514
+ return coll(collection).updateMany(
3515
+ toDriverFilter(filter),
3516
+ effective,
3517
+ updateOpts
3518
+ );
3519
+ },
3520
+ options
3521
+ );
3522
+ invalidate(collection);
3523
+ return result;
3524
+ };
3525
+ const findOneAndUpdate = async (collection, filter, update, options) => {
3526
+ const effective = stampUpdate(collection, update);
3527
+ const result = await writeRun(
3528
+ collection,
3529
+ "mongo.findOneAndUpdate",
3530
+ async (r) => {
3531
+ const fOpts = {
3532
+ ...r.driverOpts,
3533
+ session: r.sdk.session,
3534
+ maxTimeMS: r.sdk.maxTimeMS,
3535
+ hint: r.sdk.hint,
3536
+ // `after` is the ORM's default, but honor an explicit caller override.
3537
+ returnDocument: r.driverOpts.returnDocument ?? "after"
3538
+ };
3539
+ return coll(collection).findOneAndUpdate(
3540
+ toDriverFilter(filter),
3541
+ effective,
3542
+ fOpts
3543
+ );
3544
+ },
3545
+ options
3546
+ );
3547
+ invalidate(collection);
3548
+ return result;
3549
+ };
3550
+ const findOneAndReplace = async (collection, filter, replacement, options) => {
3551
+ const effective = stampReplace(collection, replacement);
3552
+ const result = await writeRun(
3553
+ collection,
3554
+ "mongo.findOneAndReplace",
3555
+ async (r) => {
3556
+ const fOpts = {
3557
+ ...r.driverOpts,
3558
+ session: r.sdk.session,
3559
+ maxTimeMS: r.sdk.maxTimeMS,
3560
+ hint: r.sdk.hint,
3561
+ // `after` is the ORM's default, but honor an explicit caller override.
3562
+ returnDocument: r.driverOpts.returnDocument ?? "after"
3563
+ };
3564
+ return coll(collection).findOneAndReplace(
3565
+ toDriverFilter(filter),
3566
+ effective,
3567
+ fOpts
3568
+ );
3569
+ },
3570
+ options
3571
+ );
3572
+ invalidate(collection);
3573
+ return result;
3574
+ };
3575
+ const replaceOne = async (collection, filter, replacement, options) => {
3576
+ const result = await writeRun(
3577
+ collection,
3578
+ "mongo.replaceOne",
3579
+ async (r) => {
3580
+ const repOpts = {
3581
+ ...r.driverOpts,
3582
+ session: r.sdk.session
3583
+ };
3584
+ return coll(collection).replaceOne(
3585
+ toDriverFilter(filter),
3586
+ stampReplace(collection, replacement),
3587
+ repOpts
3588
+ );
3589
+ },
3590
+ options
3591
+ );
3592
+ invalidate(collection);
3593
+ return result;
3594
+ };
3595
+ const deleteOne = async (collection, filter, options) => {
3596
+ await runHooks(opts.hooks, String(collection), "beforeDelete", {
3597
+ collection: String(collection),
3598
+ filter
3599
+ });
3600
+ const result = await writeRun(
3601
+ collection,
3602
+ "mongo.deleteOne",
3603
+ async (r) => {
3604
+ const delOpts = {
3605
+ ...r.driverOpts,
3606
+ session: r.sdk.session
3607
+ };
3608
+ return coll(collection).deleteOne(toDriverFilter(filter), delOpts);
3609
+ },
3610
+ options
3611
+ );
3612
+ invalidate(collection);
3613
+ await runHooks(opts.hooks, String(collection), "afterDelete", {
3614
+ collection: String(collection),
3615
+ filter
3616
+ });
3617
+ return result;
3618
+ };
3619
+ const deleteMany = async (collection, filter, options) => {
3620
+ const result = await writeRun(
3621
+ collection,
3622
+ "mongo.deleteMany",
3623
+ async (r) => {
3624
+ const delOpts = {
3625
+ ...r.driverOpts,
3626
+ session: r.sdk.session
3627
+ };
3628
+ return coll(collection).deleteMany(toDriverFilter(filter), delOpts);
3629
+ },
3630
+ options
3631
+ );
3632
+ invalidate(collection);
3633
+ return result;
3634
+ };
3635
+ const findOneAndDelete = async (collection, filter, options) => {
3636
+ const result = await writeRun(
3637
+ collection,
3638
+ "mongo.findOneAndDelete",
3639
+ async (r) => {
3640
+ const fOpts = {
3641
+ ...r.driverOpts,
3642
+ session: r.sdk.session,
3643
+ maxTimeMS: r.sdk.maxTimeMS,
3644
+ hint: r.sdk.hint
3645
+ };
3646
+ return coll(collection).findOneAndDelete(
3647
+ toDriverFilter(filter),
3648
+ fOpts
3649
+ );
3650
+ },
3651
+ options
3652
+ );
3653
+ invalidate(collection);
3654
+ return result;
3655
+ };
3656
+ const softDeleteOne = async (collection, filter, options) => {
3657
+ const active = mergeMongoActiveFilter(true, filter);
3658
+ const result = await writeRun(
3659
+ collection,
3660
+ "mongo.softDeleteOne",
3661
+ async (r) => {
3662
+ const updateOpts = {
3663
+ ...r.driverOpts,
3664
+ session: r.sdk.session
3665
+ };
3666
+ return coll(collection).updateOne(
3667
+ active,
3668
+ { $set: { deletedAt: /* @__PURE__ */ new Date() } },
3669
+ updateOpts
3670
+ );
3671
+ },
3672
+ options
3673
+ );
3674
+ invalidate(collection);
3675
+ return result;
3676
+ };
3677
+ const upsert = async (collection, filter, update, options) => {
3678
+ const formatted = stampUpdate(collection, update);
3679
+ const result = await writeRun(
3680
+ collection,
3681
+ "mongo.upsert",
3682
+ async (r) => {
3683
+ const updateOpts = {
3684
+ ...r.driverOpts,
3685
+ session: r.sdk.session,
3686
+ upsert: true
3687
+ };
3688
+ return coll(collection).updateOne(toDriverFilter(filter), formatted, updateOpts);
3689
+ },
3690
+ options
3691
+ );
3692
+ invalidate(collection);
3693
+ return result;
3694
+ };
3695
+ const bulkUpsert = async (collection, operations, options) => {
3696
+ const writes = operations.map((op) => ({
3697
+ updateOne: {
3698
+ filter: op.filter,
3699
+ update: stampUpdate(collection, op.update),
3700
+ upsert: op.upsert ?? true
3701
+ }
3702
+ }));
3703
+ if (operations.length > MAX_BATCH_OPS) {
3704
+ throw new BadRequest(
3705
+ `bulkUpsert: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
3706
+ );
3707
+ }
3708
+ const result = await writeRun(
3709
+ collection,
3710
+ "mongo.bulkUpsert",
3711
+ async (r) => {
3712
+ const bulkOpts = {
3713
+ ...r.driverOpts,
3714
+ session: r.sdk.session
3715
+ };
3716
+ return coll(collection).bulkWrite(writes, bulkOpts);
3717
+ },
3718
+ options
3719
+ );
3720
+ invalidate(collection);
3721
+ return result;
3722
+ };
3723
+ const bulkWrite = async (collection, operations, options) => {
3724
+ if (operations.length > MAX_BATCH_OPS) {
3725
+ throw new BadRequest(
3726
+ `bulkWrite: too many operations (${operations.length} > ${MAX_BATCH_OPS}); split the batch`
3727
+ );
3728
+ }
3729
+ const result = await writeRun(
3730
+ collection,
3731
+ "mongo.bulkWrite",
3732
+ async (r) => {
3733
+ const bulkOpts = {
3734
+ ...r.driverOpts,
3735
+ session: r.sdk.session
3736
+ };
3737
+ return coll(collection).bulkWrite(operations, bulkOpts);
3738
+ },
3739
+ options
3740
+ );
3741
+ invalidate(collection);
3742
+ return result;
3743
+ };
3744
+ const updateWithVersion = async (collection, filter, update, options) => {
3745
+ const existing = await getOne(collection, filter, {
3746
+ ...options,
3747
+ cache: false
3748
+ });
3749
+ if (!existing) return { ok: false, reason: "not_found" };
3750
+ const current = existing.__v;
3751
+ const versionFilter = current === void 0 ? { __v: { $exists: false } } : { __v: current };
3752
+ const formatted = stampUpdate(collection, update);
3753
+ const versionedUpdate = {
3754
+ ...formatted,
3755
+ $inc: { ...formatted.$inc, __v: 1 }
3756
+ };
3757
+ const result = await findOneAndUpdate(
3758
+ collection,
3759
+ { ...filter, ...versionFilter },
3760
+ versionedUpdate,
3761
+ { ...options, returnDocument: "after" }
3762
+ );
3763
+ if (result === null) return { ok: false, reason: "version_conflict" };
3764
+ return { ok: true, doc: result };
3765
+ };
3766
+ return {
3767
+ insertOne,
3768
+ insertMany,
3769
+ updateOne,
3770
+ updateMany,
3771
+ findOneAndUpdate,
3772
+ findOneAndReplace,
3773
+ deleteOne,
3774
+ deleteMany,
3775
+ softDeleteOne,
3776
+ upsert,
3777
+ bulkUpsert,
3778
+ bulkWrite,
3779
+ updateWithVersion,
3780
+ findOneAndDelete,
3781
+ replaceOne
3782
+ };
3783
+ };
3784
+
3785
+ // src/service/crud/index.ts
3786
+ var makeCrudOps = (client, dbLabel, logger, opts) => {
3787
+ const ctx = createCrudContext(client, dbLabel, logger, opts);
3788
+ const readOps = makeReadOps(ctx);
3789
+ return {
3790
+ ...readOps,
3791
+ ...makeWriteOps(ctx, readOps),
3792
+ ...makeWatchOps(ctx)
3793
+ };
3794
+ };
3795
+ var tag = (value) => {
3796
+ if (value instanceof ObjectId) return { t: "oid", v: value.toHexString() };
3797
+ if (value instanceof Date) return { t: "date", v: value.toISOString() };
3798
+ if (typeof value === "string") return { t: "string", v: value };
3799
+ if (typeof value === "number") return { t: "number", v: value };
3800
+ if (typeof value === "boolean") return { t: "boolean", v: value };
3801
+ if (value === null || value === void 0) return { t: "null" };
3802
+ throw new BadRequest(
3803
+ `paginateCursor: unsupported sort value type "${typeof value}" \u2014 key values must be ObjectId, Date, string, number, boolean, or null`
3804
+ );
3805
+ };
3806
+ var untag = (tagged) => {
3807
+ switch (tagged.t) {
3808
+ case "oid":
3809
+ return new ObjectId(tagged.v);
3810
+ case "date":
3811
+ return new Date(tagged.v);
3812
+ case "string":
3813
+ return tagged.v;
3814
+ case "number":
3815
+ return tagged.v;
3816
+ case "boolean":
3817
+ return tagged.v;
3818
+ case "null":
3819
+ return null;
3820
+ }
3821
+ };
3822
+ var encodeCursor = (cursor) => Buffer.from(
3823
+ JSON.stringify({
3824
+ sort: cursor.sort,
3825
+ values: cursor.values.map(tag)
3826
+ }),
3827
+ "utf8"
3828
+ ).toString("base64url");
3829
+ var decodeCursor = (cursor) => {
3830
+ let parsed;
3831
+ try {
3832
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3833
+ } catch {
3834
+ throw new BadRequest("paginateCursor: invalid cursor");
3835
+ }
3836
+ if (!parsed || typeof parsed !== "object" || !parsed.sort || !Array.isArray(parsed.values)) {
3837
+ throw new BadRequest("paginateCursor: invalid cursor payload");
3838
+ }
3839
+ return { sort: parsed.sort, values: parsed.values.map(untag) };
3840
+ };
3841
+ var buildKeysetFilter = (sort, values, direction) => {
3842
+ const keys = Object.entries(sort);
3843
+ const ors = [];
3844
+ for (let i = 0; i < keys.length; i++) {
3845
+ const cond = {};
3846
+ for (let j = 0; j < i; j++) {
3847
+ const [f] = keys[j];
3848
+ cond[f] = values[j];
3849
+ }
3850
+ const [field, dir] = keys[i];
3851
+ const value = values[i];
3852
+ const cmp = direction === "after" ? dir === 1 ? "$gt" : "$lt" : dir === 1 ? "$lt" : "$gt";
3853
+ cond[field] = { [cmp]: value };
3854
+ ors.push(cond);
3855
+ }
3856
+ return { $or: ors };
3857
+ };
3858
+
3859
+ // src/service/pagination/cursor.ts
3860
+ var makePaginateCursor = (ctx) => {
3861
+ const { client, dbLabel, logger, opts, resolve, deps: deps2 } = ctx;
3862
+ const paginateCursor = async (collection, baseFilter, config) => {
3863
+ const sortKeys = Object.entries(config.sort);
3864
+ if (sortKeys.length === 0) {
3865
+ throw new BadRequest(
3866
+ "paginateCursor: `sort` is required (include a unique tiebreaker like `_id`)"
3867
+ );
3868
+ }
3869
+ if (config.before !== void 0 && config.after !== void 0) {
3870
+ throw new BadRequest("paginateCursor: provide either `after` or `before`, not both");
3871
+ }
3872
+ const limit = normalizePageLimit(
3873
+ { page: 1, limit: config.limit },
3874
+ config.maxLimit ?? DEFAULT_MAX_LIMIT,
3875
+ { limit: DEFAULT_FIND_LIMIT }
3876
+ ).limit;
3877
+ const backwards = config.before !== void 0 && config.after === void 0;
3878
+ const effectiveSort = backwards ? Object.fromEntries(sortKeys.map(([f, d]) => [f, d * -1])) : config.sort;
3879
+ let filter = baseFilter;
3880
+ if (config.after !== void 0 || config.before !== void 0) {
3881
+ const cursor = config.after ?? config.before;
3882
+ const decoded = decodeCursor(cursor);
3883
+ if (JSON.stringify(decoded.sort) !== JSON.stringify(config.sort)) {
3884
+ throw new BadRequest("paginateCursor: cursor was created with a different sort");
3885
+ }
3886
+ if (decoded.values.length !== sortKeys.length) {
3887
+ throw new BadRequest("paginateCursor: cursor was created with a different sort");
3888
+ }
3889
+ filter = {
3890
+ $and: [baseFilter, buildKeysetFilter(effectiveSort, decoded.values, "after")]
3891
+ };
3892
+ }
3893
+ const rows = await defineCrudOp(
3894
+ deps2,
3895
+ collection,
3896
+ "mongo.paginateCursor",
3897
+ async (r) => {
3898
+ const findOpts = {
3899
+ ...r.driverOpts,
3900
+ session: r.sdk.session,
3901
+ maxTimeMS: r.sdk.maxTimeMS,
3902
+ hint: r.sdk.hint
3903
+ };
3904
+ return client.collection(resolve(String(collection))).find(filter, findOpts).sort(effectiveSort).limit(limit + 1).toArray();
3905
+ },
3906
+ config.queryOptions
3907
+ );
3908
+ const hasMore = rows.length > limit;
3909
+ const page = backwards ? rows.slice(0, limit).reverse() : rows.slice(0, limit);
3910
+ checkDocsDrift(
3911
+ {
3912
+ logger,
3913
+ db: dbLabel,
3914
+ drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
3915
+ getSchema: opts.getSchema
3916
+ },
3917
+ String(collection),
3918
+ "mongo.paginateCursor",
3919
+ page
3920
+ );
3921
+ const boundary = backwards ? rows[limit] : page[page.length - 1];
3922
+ const nextCursor = hasMore && boundary !== void 0 ? encodeCursor({
3923
+ sort: config.sort,
3924
+ values: sortKeys.map(([f]) => boundary[f])
3925
+ }) : null;
3926
+ return { data: page, nextCursor, hasMore };
3927
+ };
3928
+ return { paginateCursor };
3929
+ };
3930
+
3931
+ // src/service/pagination/offset.ts
3932
+ var makePaginateFlexible = (ctx) => {
3933
+ const { client, dbLabel, logger, opts, resolve, deps: deps2 } = ctx;
3934
+ const paginateFlexible = async (collection, baseFilter, config = {}) => {
3935
+ const normalized = normalizePageLimit(
3936
+ { page: config.page, limit: config.limit },
3937
+ config.maxLimit ?? DEFAULT_MAX_LIMIT
3938
+ );
3939
+ const dataStages = [
3940
+ ...config.prePipeline ?? [],
3941
+ ...config.sort && Object.keys(config.sort).length > 0 ? [{ $sort: config.sort }] : [],
3942
+ { $skip: normalized.offset },
3943
+ { $limit: normalized.limit },
3944
+ ...config.postPipeline ?? []
3945
+ ];
3946
+ const pipeline = [
3947
+ { $match: baseFilter },
3948
+ {
3949
+ $facet: {
3950
+ data: dataStages,
3951
+ totalCount: [{ $count: "count" }]
3952
+ }
3953
+ }
3954
+ ];
3955
+ return defineCrudOp(
3956
+ deps2,
3957
+ collection,
3958
+ "mongo.paginateFlexible",
3959
+ async (r) => {
3960
+ const aggOptions = {
3961
+ ...r.driverOpts,
3962
+ session: r.sdk.session,
3963
+ maxTimeMS: r.sdk.maxTimeMS,
3964
+ hint: r.sdk.hint,
3965
+ batchSize: r.sdk.batchSize
3966
+ };
3967
+ const rows = await client.collection(resolve(String(collection))).aggregate(pipeline, aggOptions).toArray();
3968
+ const row = rows[0];
3969
+ const data = row?.data ?? [];
3970
+ const totalCount = row?.totalCount?.[0]?.count ?? 0;
3971
+ checkDocsDrift(
3972
+ {
3973
+ logger,
3974
+ db: dbLabel,
3975
+ drift: resolveDriftMode(config.queryOptions?.drift, opts.drift),
3976
+ getSchema: opts.getSchema
3977
+ },
3978
+ String(collection),
3979
+ "mongo.paginateFlexible",
3980
+ data
3981
+ );
3982
+ return buildPaginationResult(data, totalCount, normalized.page, normalized.limit);
3983
+ },
3984
+ config.queryOptions
3985
+ );
3986
+ };
3987
+ return { paginateFlexible };
3988
+ };
3989
+
3990
+ // src/service/pagination/index.ts
3991
+ var makePaginationOps = (client, dbLabel, logger, opts) => {
3992
+ const resolve = opts.resolveCollectionName;
3993
+ const deps2 = makeOpDeps(logger, dbLabel, resolve, opts.wrapMongoErrors === true);
3994
+ const ctx = {
3995
+ client,
3996
+ dbLabel,
3997
+ logger,
3998
+ opts,
3999
+ resolve,
4000
+ deps: deps2
4001
+ };
4002
+ return {
4003
+ ...makePaginateFlexible(ctx),
4004
+ ...makePaginateCursor(ctx)
4005
+ };
4006
+ };
4007
+
4008
+ // src/service/schema-ops.ts
4009
+ var indexKeyOf = (key) => JSON.stringify(
4010
+ Object.keys(key).sort().map((k) => [k, key[k]])
4011
+ );
4012
+ var makeSchemaOps = (client, _logger, opts) => {
4013
+ const createSchema = async (collection, createCollectionOptions) => {
4014
+ const physical = opts.resolveCollectionName(collection);
4015
+ const def = opts.getDefinition(collection);
4016
+ const validator = def?.schema ? toMongoValidator(def.schema) : void 0;
4017
+ try {
4018
+ await client.createCollection(physical, {
4019
+ ...validator ? { validator } : {},
4020
+ ...createCollectionOptions ?? {}
4021
+ });
4022
+ } catch (err) {
4023
+ if (err?.code === 48) {
4024
+ throw new DomainError("COLLECTION_EXISTS", `Collection "${collection}" already exists`, {
4025
+ physicalCollection: physical
4026
+ });
4027
+ }
4028
+ throw err;
4029
+ }
4030
+ const indexes = def?.indexes ?? [];
4031
+ if (indexes.length > 0) {
4032
+ try {
4033
+ await client.collection(physical).createIndexes(
4034
+ indexes.map((index) => ({
4035
+ key: index.key,
4036
+ ...index.options ?? {}
4037
+ }))
4038
+ );
4039
+ } catch (err) {
4040
+ await client.dropCollection(physical).catch(() => {
4041
+ });
4042
+ throw err;
4043
+ }
4044
+ }
4045
+ };
4046
+ const updateSchema = async (collection, schema) => {
4047
+ const physical = opts.resolveCollectionName(collection);
4048
+ const def = opts.getDefinition(collection);
4049
+ const resolved = schema ?? def?.schema;
4050
+ if (!resolved) {
4051
+ throw new Error(`No schema available for collection "${collection}"`);
4052
+ }
4053
+ await client.command({
4054
+ collMod: physical,
4055
+ validator: toMongoValidator(resolved)
4056
+ });
4057
+ };
4058
+ const syncIndexes = async (collection) => {
4059
+ const physical = opts.resolveCollectionName(collection);
4060
+ const def = opts.getDefinition(collection);
4061
+ const declared = def?.indexes ?? [];
4062
+ const handle = client.collection(physical);
4063
+ const existing = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
4064
+ const existingByKey = new Map(
4065
+ existing.map((i) => [indexKeyOf(i.key), i.name])
4066
+ );
4067
+ const declaredByKey = new Map(declared.map((d) => [indexKeyOf(d.key), d]));
4068
+ const created = [];
4069
+ const dropped = [];
4070
+ for (const [key] of declaredByKey) {
4071
+ if (!existingByKey.has(key)) {
4072
+ await handle.createIndex(
4073
+ declaredByKey.get(key).key,
4074
+ declaredByKey.get(key).options
4075
+ );
4076
+ }
4077
+ }
4078
+ const after = (await handle.listIndexes().toArray()).filter((i) => i.name !== "_id_");
4079
+ const afterByKey = new Map(after.map((i) => [indexKeyOf(i.key), i.name]));
4080
+ for (const [key, spec] of declaredByKey) {
4081
+ if (!existingByKey.has(key)) {
4082
+ created.push(spec.options?.name ?? afterByKey.get(key) ?? JSON.stringify(spec.key));
4083
+ }
4084
+ }
4085
+ for (const [key, name] of existingByKey) {
4086
+ if (!declaredByKey.has(key)) {
4087
+ await handle.dropIndex(name);
4088
+ dropped.push(name);
4089
+ }
4090
+ }
4091
+ return { created, dropped };
4092
+ };
4093
+ return { createSchema, updateSchema, syncIndexes };
4094
+ };
4095
+
4096
+ // src/service/transaction.ts
4097
+ var makeTransactionOps = (client, handle, logger, _config, opts) => {
4098
+ const transaction = async (fn, options) => {
4099
+ const session = client.startSession();
4100
+ try {
4101
+ return await session.withTransaction(async () => fn(session), options);
4102
+ } catch (err) {
4103
+ const message = err instanceof Error ? err.message : "";
4104
+ const code = err?.code;
4105
+ const unsupported = code === 20 || /Transaction numbers are only allowed/i.test(message) || /does not support retryable writes/i.test(message);
4106
+ if (unsupported) {
4107
+ logger.warn?.({ code }, "transactions unsupported, falling back to non-transactional");
4108
+ return fn(null);
4109
+ }
4110
+ if (opts.wrapMongoErrors) {
4111
+ throw mapMongoDriverError(err, { db: handle.databaseName, op: "transaction" });
4112
+ }
4113
+ throw err;
4114
+ } finally {
4115
+ await session.endSession();
4116
+ }
4117
+ };
4118
+ const migrate = async (name, fn, filePath) => {
4119
+ const journal = handle.collection(
4120
+ opts.migrationCollectionPhysical
4121
+ );
4122
+ await transaction(async (session) => {
4123
+ const sessionOpts = session ? { session } : {};
4124
+ const claim = await journal.findOneAndUpdate(
4125
+ { name },
4126
+ {
4127
+ $setOnInsert: {
4128
+ name,
4129
+ appliedAt: /* @__PURE__ */ new Date(),
4130
+ ...filePath ? { file: filePath } : {}
4131
+ }
4132
+ },
4133
+ { upsert: true, returnDocument: "before", ...sessionOpts }
4134
+ );
4135
+ if (claim) {
4136
+ logger.info({ name }, "migration already applied, skipping");
4137
+ return;
4138
+ }
4139
+ try {
4140
+ await fn(session);
4141
+ } catch (err) {
4142
+ await journal.deleteOne({ name }, sessionOpts).catch(() => {
4143
+ });
4144
+ throw err;
4145
+ }
4146
+ });
4147
+ };
4148
+ return { transaction, migrate };
4149
+ };
4150
+
4151
+ // src/service/manager.ts
4152
+ var buildManager = (deps2, dbKey, dbName, client) => {
4153
+ const { dbClients, config, logger, sharedCache, dedupeReads, inFlight, collectionRefs } = deps2;
4154
+ const definition = dbClients[dbKey];
4155
+ const handle = client.db(dbName);
4156
+ const baseResolveCollectionName = createResolveCollectionName(definition);
4157
+ const registry = /* @__PURE__ */ new Map();
4158
+ const hooks = {};
4159
+ const timestamps = {};
4160
+ for (const [logical, value] of Object.entries(definition.collections)) {
4161
+ const def = asCollectionDefinition(value);
4162
+ registry.set(logical, def);
4163
+ hooks[logical] = def.hooks;
4164
+ if (def.timestamps) timestamps[logical] = def.timestamps;
4165
+ collectionRefs.push({ db: handle, collection: baseResolveCollectionName(logical) });
4166
+ }
4167
+ const getDefinition = (logical) => registry.get(logical);
4168
+ const resolveCollectionName = (logical) => {
4169
+ if (!registry.has(logical) && !logical.startsWith("_")) {
4170
+ throw new BadRequest(`Unknown collection "${logical}"`);
4171
+ }
4172
+ return baseResolveCollectionName(logical);
4173
+ };
4174
+ const crud = makeCrudOps(handle, dbName, logger, {
4175
+ resolveCollectionName,
4176
+ wrapMongoErrors: deps2.wrapMongoErrors,
4177
+ cache: sharedCache,
4178
+ dedupeReads,
4179
+ inFlight,
4180
+ hooks,
4181
+ timestamps,
4182
+ drift: deps2.drift,
4183
+ getSchema: (logical) => getDefinition(logical)?.schema
4184
+ });
4185
+ const pagination = makePaginationOps(handle, dbName, logger, {
4186
+ resolveCollectionName,
4187
+ wrapMongoErrors: deps2.wrapMongoErrors,
4188
+ drift: deps2.drift,
4189
+ getSchema: (logical) => getDefinition(logical)?.schema
4190
+ });
4191
+ const aggregation = makeAggregationOps(handle, dbName, logger, {
4192
+ resolveCollectionName,
4193
+ wrapMongoErrors: deps2.wrapMongoErrors
4194
+ });
4195
+ const schema = makeSchemaOps(handle, logger, { resolveCollectionName, getDefinition });
4196
+ const transactionOps = makeTransactionOps(client, handle, logger, config, {
4197
+ migrationCollectionPhysical: resolveCollectionName("_migrations"),
4198
+ wrapMongoErrors: deps2.wrapMongoErrors
4199
+ });
4200
+ const populator = makePopulator({
4201
+ findMany: async (logical, filter, options) => {
4202
+ const physical = resolveCollectionName(logical);
4203
+ return handle.collection(physical).find(filter, {
4204
+ projection: options?.projection,
4205
+ limit: options?.limit ?? 1e4
4206
+ }).toArray();
4207
+ }
4208
+ });
4209
+ const populate = (async (docs, relations, options = {}) => {
4210
+ const result = await populator.populate(docs, relations, options);
4211
+ return result;
4212
+ });
4213
+ return {
4214
+ ...crud,
4215
+ ...pagination,
4216
+ ...aggregation,
4217
+ ...schema,
4218
+ ...transactionOps,
4219
+ populate,
4220
+ client: handle,
4221
+ db: deps2.db
4222
+ };
4223
+ };
4224
+
4225
+ // src/service/index.ts
4226
+ var createMongoService = (dbClients, configOverrides = {}) => {
4227
+ const runtime = resolveRuntimeConfig(configOverrides);
4228
+ const { config, logger, sharedCache, dedupeReads } = runtime;
4229
+ const db = {};
4230
+ const openedClients = /* @__PURE__ */ new Map();
4231
+ const inFlight = new InFlight();
4232
+ const getDbUrl = makeGetDbUrl(dbClients, config);
4233
+ const collectionRefs = [];
4234
+ const cacheInvalidator = sharedCache !== void 0 && configOverrides.cacheWatch === true ? new CacheInvalidator({ cache: sharedCache, logger }) : void 0;
4235
+ const baseMakeConnections = makeConnectionFactory(
4236
+ dbClients,
4237
+ db,
4238
+ getDbUrl,
4239
+ ((key, dbName, client) => buildManager(
4240
+ {
4241
+ dbClients,
4242
+ config,
4243
+ logger,
4244
+ sharedCache,
4245
+ dedupeReads,
4246
+ inFlight,
4247
+ wrapMongoErrors: runtime.wrapMongoErrors,
4248
+ drift: configOverrides.drift,
4249
+ collectionRefs,
4250
+ db
4251
+ },
4252
+ key,
4253
+ dbName,
4254
+ client
4255
+ )),
4256
+ openedClients
4257
+ );
4258
+ const makeConnections = async (names) => {
4259
+ await baseMakeConnections(names);
4260
+ await cacheInvalidator?.start(collectionRefs);
4261
+ };
4262
+ const closeConnections2 = async () => {
4263
+ await cacheInvalidator?.stop();
4264
+ collectionRefs.length = 0;
4265
+ await closeConnections(openedClients);
4266
+ };
4267
+ const eachDb = async (fn) => {
4268
+ return Promise.all(Object.keys(db).map((key) => fn(db[key], key.replace(/Client$/, ""))));
4269
+ };
4270
+ const health = createHealth(db);
4271
+ return {
4272
+ db,
4273
+ makeConnections,
4274
+ closeConnections: closeConnections2,
4275
+ eachDb,
4276
+ health,
4277
+ getDbUrl,
4278
+ config,
4279
+ logger,
4280
+ ObjectId: ObjectId
4281
+ };
4282
+ };
4283
+
4284
+ // src/shared/merge-filters.ts
4285
+ var mergeMongoFilters = (...parts) => {
4286
+ const out = {};
4287
+ for (const part of parts) {
4288
+ if (!part) continue;
4289
+ Object.assign(out, part);
4290
+ }
4291
+ return out;
4292
+ };
4293
+
4294
+ // src/shared/strip-primary-key.ts
4295
+ var stripDocumentId = (doc) => {
4296
+ if (!("_id" in doc)) return doc;
4297
+ const { _id, ...rest } = doc;
4298
+ return rest;
4299
+ };
4300
+ var stripPrimaryKey = (patch, key) => {
4301
+ if (!(key in patch)) return patch;
4302
+ const { [key]: _, ...rest } = patch;
4303
+ return rest;
4304
+ };
4305
+
4306
+ // src/toolkit.ts
4307
+ var createMongoToolkit = (dbClients, options = {}) => {
4308
+ const service = createMongoService(dbClients, options);
4309
+ const migrations = createMongoMigrationRunner(service, {
4310
+ migrationDir: service.config.migrationDir
4311
+ });
4312
+ return { service, migrations };
4313
+ };
4314
+
4315
+ // src/utils/memoize.ts
4316
+ var makeStore = (maxSize) => {
4317
+ if (maxSize <= 0) {
4318
+ const map = /* @__PURE__ */ new Map();
4319
+ return {
4320
+ get: (k) => map.get(k),
4321
+ set: (k, v) => {
4322
+ map.set(k, v);
4323
+ },
4324
+ has: (k) => map.has(k),
4325
+ delete: (k) => {
4326
+ map.delete(k);
4327
+ }
4328
+ };
4329
+ }
4330
+ return new LRU({ max: maxSize });
4331
+ };
4332
+ var createCachedFactory = (factory, options = {}) => {
4333
+ const maxSize = options.maxSize ?? 100;
4334
+ const cache = makeStore(maxSize);
4335
+ return (key) => {
4336
+ const existing = cache.get(key);
4337
+ if (existing !== void 0) return existing;
4338
+ const value = factory(key);
4339
+ cache.set(key, value);
4340
+ return value;
4341
+ };
4342
+ };
4343
+ var createCachedAsyncFactory = (factory, options = {}) => {
4344
+ const maxSize = options.maxSize ?? 100;
4345
+ const cache = makeStore(maxSize);
4346
+ const inFlight = /* @__PURE__ */ new Map();
4347
+ return (key) => {
4348
+ const inflight = inFlight.get(key);
4349
+ if (inflight) return inflight;
4350
+ const cached = cache.get(key);
4351
+ if (cached) return cached;
4352
+ const promise = factory(key);
4353
+ inFlight.set(key, promise);
4354
+ cache.set(key, promise);
4355
+ promise.then(
4356
+ () => {
4357
+ inFlight.delete(key);
4358
+ },
4359
+ () => {
4360
+ inFlight.delete(key);
4361
+ if (cache.get(key) === promise) cache.delete(key);
4362
+ }
4363
+ );
4364
+ return promise;
4365
+ };
4366
+ };
4367
+
4368
+ 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 };
4369
+ //# sourceMappingURL=index.js.map
4370
+ //# sourceMappingURL=index.js.map