@classytic/mongokit 3.24.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +12 -2
  2. package/dist/{PaginationEngine-BnZ24ehC.d.mts → PaginationEngine-CBS1XGzp.d.mts} +2 -1
  3. package/dist/{Repository-ClOraFjT.mjs → Repository-BFBTAMlA.mjs} +177 -54
  4. package/dist/actions/index.d.mts +1 -1
  5. package/dist/adapter/index.d.mts +1 -1
  6. package/dist/adapter/index.mjs +1 -1
  7. package/dist/{adapter-BWgJ_Jdc.mjs → adapter-CLvUHiTW.mjs} +1 -1
  8. package/dist/ai/index.d.mts +1 -1
  9. package/dist/better-auth/index.mjs +2 -2
  10. package/dist/core-62oo3N1C.d.mts +50 -0
  11. package/dist/{create-repository-CVupgeRG.mjs → create-repository-2DMwkYVz.mjs} +2 -2
  12. package/dist/{create-repository-Cn1LkvL7.d.mts → create-repository-D4ZU94b3.d.mts} +33 -5
  13. package/dist/{id-resolution-P2OzcyRl.mjs → id-resolution-BqDrk0S2.mjs} +12 -1
  14. package/dist/{index-DLQ6pjal.d.mts → index-DceInCTX.d.mts} +3 -1
  15. package/dist/index.d.mts +70 -213
  16. package/dist/index.mjs +1071 -922
  17. package/dist/{mongooseToJsonSchema-CVnrKFDq.mjs → mongooseToJsonSchema-CByt1VhQ.mjs} +1 -1
  18. package/dist/{multi-tenant.plugin-BjXXuc_g.d.mts → multi-tenant.plugin-DVyVGREb.d.mts} +117 -18
  19. package/dist/operations-C8IcCHF9.d.mts +463 -0
  20. package/dist/pagination/PaginationEngine.d.mts +1 -1
  21. package/dist/pagination-jSUdrACK.d.mts +158 -0
  22. package/dist/plugin-options-vFufQZMZ.d.mts +125 -0
  23. package/dist/plugins/index.d.mts +4 -4
  24. package/dist/plugins/index.mjs +3 -3
  25. package/dist/query/primitives/coercion.mjs +1 -1
  26. package/dist/query/primitives/geo.d.mts +1 -1
  27. package/dist/repository-DCHRWRXw.d.mts +511 -0
  28. package/dist/testkit/index.d.mts +1 -1
  29. package/dist/testkit/index.mjs +1 -1
  30. package/dist/{timestamp.plugin-BhSX0Lu_.mjs → timestamp.plugin-ennbatTW.mjs} +192 -46
  31. package/dist/{to-plain-g8-gsVxZ.d.mts → to-plain-DkF5px0R.d.mts} +12 -16
  32. package/dist/type-utils-CLzRFtIA.d.mts +163 -0
  33. package/dist/utils/index.d.mts +2 -2
  34. package/dist/utils/index.mjs +3 -3
  35. package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-Cm0Ttnd8.d.mts} +140 -2
  36. package/dist/{validation-chain.plugin-Dtt4tO7R.mjs → validation-chain.plugin-k9joH2qw.mjs} +149 -2
  37. package/package.json +7 -5
  38. package/dist/types-BtMuhoX5.d.mts +0 -1830
@@ -1,4 +1,4 @@
1
- import { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-ClOraFjT.mjs";
1
+ import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-BFBTAMlA.mjs";
2
2
  import { n as warn } from "./logger-K7dPmu2U.mjs";
3
3
  import { t as createError } from "./error-BuByZwKU.mjs";
4
4
  import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
@@ -108,12 +108,34 @@ function auditLogPlugin(logger) {
108
108
  * Audit Trail Plugin
109
109
  *
110
110
  * Persists operation audit entries to a MongoDB collection.
111
- * Fire-and-forget: writes happen async and never block or fail the main operation.
111
+ *
112
+ * Two delivery modes (`mode` option):
113
+ *
114
+ * - `'best-effort'` (default): fire-and-forget — writes happen async and
115
+ * never block or fail the main operation. Suitable for observability,
116
+ * debugging, and eventually-consistent activity feeds. NOT suitable as a
117
+ * compliance ledger: an entry can be written for a transaction that later
118
+ * aborts, and the process can die after the business write but before the
119
+ * audit write lands.
120
+ *
121
+ * - `'transactional'`: the audit insert is awaited inside the operation and
122
+ * joins the operation's `session` when one is present. Inside a
123
+ * `withTransaction` block this makes business write + audit entry atomic —
124
+ * both commit or both roll back. A failed audit write fails the operation
125
+ * (after routing through `onWriteError`). This is the mode to use for
126
+ * compliance-grade trails (SOC 2 / PCI / HIPAA) — but note it is atomic
127
+ * ONLY inside an actual transaction; without one, an audit failure rejects
128
+ * the call after the business write has already committed. Call
129
+ * `ensureAuditTrailReady()` at boot so collection/index creation doesn't
130
+ * happen inside the first transaction. For durable *asynchronous* auditing,
131
+ * write an outbox entry in the same transaction instead and relay it
132
+ * separately (see `@classytic/primitives/outbox`).
112
133
  *
113
134
  * Features:
114
135
  * - Tracks create, update, delete operations
115
136
  * - Field-level change tracking (before/after diff on updates)
116
- * - TTL auto-cleanup via MongoDB TTL index
137
+ * - Connection-aware: models register on the connection you pass (multi-DB safe)
138
+ * - TTL auto-cleanup via MongoDB TTL index — retention conflicts throw
117
139
  * - Custom metadata per entry (IP, user-agent, etc.)
118
140
  * - Shared `audit_trails` collection across all models
119
141
  *
@@ -121,9 +143,11 @@ function auditLogPlugin(logger) {
121
143
  * ```typescript
122
144
  * const repo = new Repository(Job, [
123
145
  * auditTrailPlugin({
146
+ * mode: 'transactional',
124
147
  * operations: ['create', 'update', 'delete'],
125
148
  * trackChanges: true,
126
149
  * ttlDays: 90,
150
+ * connection: jobsConnection, // omit to use mongoose.connection
127
151
  * metadata: (context) => ({
128
152
  * ip: context.req?.ip,
129
153
  * }),
@@ -131,10 +155,56 @@ function auditLogPlugin(logger) {
131
155
  * ]);
132
156
  * ```
133
157
  */
134
- const modelCache = /* @__PURE__ */ new Map();
135
- function getAuditModel(collectionName, ttlDays) {
136
- const existing = modelCache.get(collectionName);
137
- if (existing) return existing;
158
+ /**
159
+ * Per-connection model cache. WeakMap keyed by the Connection object so a
160
+ * closed/discarded connection's models can be garbage collected, and so two
161
+ * connections never share a model even when they use the same collection
162
+ * name (each connection may point at a different database).
163
+ */
164
+ const modelCaches = /* @__PURE__ */ new WeakMap();
165
+ function ttlDaysToSeconds(ttlDays) {
166
+ return ttlDays !== void 0 && ttlDays > 0 ? ttlDays * 24 * 60 * 60 : null;
167
+ }
168
+ /**
169
+ * Introspect an already-compiled model (hot reload, duplicate registration
170
+ * outside our cache) for the TTL its schema installed on `timestamp`.
171
+ */
172
+ function resolveExistingTtlSeconds(model) {
173
+ for (const [fields, opts] of model.schema.indexes()) {
174
+ const expireAfterSeconds = opts?.expireAfterSeconds;
175
+ if (fields.timestamp !== void 0 && typeof expireAfterSeconds === "number") return expireAfterSeconds;
176
+ }
177
+ return null;
178
+ }
179
+ function ttlConflictError(collectionName, existing, requested) {
180
+ const fmt = (s) => s === null ? "no TTL" : `${s / 86400} day(s)`;
181
+ return /* @__PURE__ */ new Error(`[auditTrailPlugin] TTL conflict for audit collection '${collectionName}': already registered with ${fmt(existing)}, now requested with ${fmt(requested)}. Retention is collection-level configuration — every auditTrailPlugin()/AuditTrailQuery touching the same collection on the same connection must pass the same ttlDays. Changing retention on an existing collection requires an index migration outside the plugin.`);
182
+ }
183
+ function getAuditModel(connection, collectionName, ttlDays, ttlPolicy = "declare") {
184
+ const conn = connection ?? mongoose.connection;
185
+ const requestedTtl = ttlDaysToSeconds(ttlDays);
186
+ const reuseExisting = ttlPolicy === "reuse-existing" && ttlDays === void 0;
187
+ let cache = modelCaches.get(conn);
188
+ if (!cache) {
189
+ cache = /* @__PURE__ */ new Map();
190
+ modelCaches.set(conn, cache);
191
+ }
192
+ const cached = cache.get(collectionName);
193
+ if (cached) {
194
+ if (!reuseExisting && cached.ttlSeconds !== requestedTtl) throw ttlConflictError(collectionName, cached.ttlSeconds, requestedTtl);
195
+ return cached.model;
196
+ }
197
+ const modelName = `AuditTrail_${collectionName}`;
198
+ const existing = conn.models[modelName] ?? (conn === mongoose.connection ? mongoose.models[modelName] : void 0);
199
+ if (existing) {
200
+ const existingTtl = resolveExistingTtlSeconds(existing);
201
+ if (!reuseExisting && existingTtl !== requestedTtl) throw ttlConflictError(collectionName, existingTtl, requestedTtl);
202
+ cache.set(collectionName, {
203
+ model: existing,
204
+ ttlSeconds: existingTtl
205
+ });
206
+ return existing;
207
+ }
138
208
  const schema = new mongoose.Schema({
139
209
  model: {
140
210
  type: String,
@@ -169,8 +239,7 @@ function getAuditModel(collectionName, ttlDays) {
169
239
  metadata: { type: mongoose.Schema.Types.Mixed },
170
240
  timestamp: {
171
241
  type: Date,
172
- default: Date.now,
173
- index: true
242
+ default: Date.now
174
243
  }
175
244
  }, {
176
245
  collection: collectionName,
@@ -186,13 +255,13 @@ function getAuditModel(collectionName, ttlDays) {
186
255
  userId: 1,
187
256
  timestamp: -1
188
257
  });
189
- if (ttlDays !== void 0 && ttlDays > 0) {
190
- const ttlSeconds = ttlDays * 24 * 60 * 60;
191
- schema.index({ timestamp: 1 }, { expireAfterSeconds: ttlSeconds });
192
- }
193
- const modelName = `AuditTrail_${collectionName}`;
194
- const model = mongoose.models[modelName] || mongoose.model(modelName, schema);
195
- modelCache.set(collectionName, model);
258
+ if (requestedTtl !== null) schema.index({ timestamp: 1 }, { expireAfterSeconds: requestedTtl });
259
+ else schema.index({ timestamp: 1 });
260
+ const model = conn.model(modelName, schema);
261
+ cache.set(collectionName, {
262
+ model,
263
+ ttlSeconds: requestedTtl
264
+ });
196
265
  return model;
197
266
  }
198
267
  /** Compute field-level diff between previous and updated document */
@@ -232,30 +301,63 @@ function deepEqual(a, b) {
232
301
  function getUserId(context) {
233
302
  return context.user?._id || context.user?.id;
234
303
  }
304
+ /** Resolve the effective id field for an operation: per-call override > repo config > '_id'. */
305
+ function getEffectiveIdField(repo, context) {
306
+ return context.idField ?? repo.idField ?? "_id";
307
+ }
308
+ function invokeWriteErrorCallback(onWriteError, err, entry) {
309
+ if (!onWriteError) {
310
+ warn(`[auditTrailPlugin] Failed to write audit entry: ${err.message}`);
311
+ return;
312
+ }
313
+ try {
314
+ onWriteError(err, entry);
315
+ } catch (cbErr) {
316
+ warn(`[auditTrailPlugin] onWriteError callback itself threw: ${cbErr.message}`);
317
+ }
318
+ }
235
319
  /**
236
- * Fire-and-forget audit write. Never throws into the caller's promise
237
- * chain (audit failures must not break the primary operation), but
238
- * routes errors to `onWriteError` for compliance hosts that need
239
- * surface visibility. Default behavior: `console.warn`-only.
320
+ * Best-effort audit write. Never throws into the caller's promise chain
321
+ * (audit failures must not break the primary operation), but routes errors
322
+ * to `onWriteError` for hosts that need surface visibility.
323
+ *
324
+ * Deliberately does NOT join the operation's session: by the time the
325
+ * microtask runs, a transaction may already be committed or aborted —
326
+ * joining it late would either throw or attach the entry to a doomed
327
+ * transaction. Session-consistent delivery is exactly what
328
+ * `mode: 'transactional'` is for.
240
329
  */
241
- function writeAudit(AuditModel, entry, onWriteError) {
330
+ function writeAuditBestEffort(AuditModel, entry, onWriteError) {
242
331
  Promise.resolve().then(() => {
243
332
  AuditModel.create({
244
333
  ...entry,
245
334
  timestamp: /* @__PURE__ */ new Date()
246
335
  }).catch((err) => {
247
- if (onWriteError) try {
248
- onWriteError(err, entry);
249
- } catch (cbErr) {
250
- warn(`[auditTrailPlugin] onWriteError callback itself threw: ${cbErr.message}`);
251
- }
252
- else warn(`[auditTrailPlugin] Failed to write audit entry: ${err.message}`);
336
+ invokeWriteErrorCallback(onWriteError, err, entry);
253
337
  });
254
338
  });
255
339
  }
340
+ /**
341
+ * Transactional audit write. Awaited by the operation's after-hook and
342
+ * attached to the operation's session when present — inside a transaction
343
+ * the entry commits/aborts atomically with the business write. A failed
344
+ * insert rethrows (after `onWriteError`) so the operation fails rather than
345
+ * silently succeeding without its audit entry.
346
+ */
347
+ async function writeAuditTransactional(AuditModel, entry, session, onWriteError) {
348
+ try {
349
+ await AuditModel.create([{
350
+ ...entry,
351
+ timestamp: /* @__PURE__ */ new Date()
352
+ }], session ? { session } : {});
353
+ } catch (err) {
354
+ invokeWriteErrorCallback(onWriteError, err, entry);
355
+ throw err;
356
+ }
357
+ }
256
358
  const snapshots = /* @__PURE__ */ new WeakMap();
257
359
  function auditTrailPlugin(options = {}) {
258
- const { operations = [
360
+ const { mode = "best-effort", connection, operations = [
259
361
  "create",
260
362
  "update",
261
363
  "delete"
@@ -268,12 +370,20 @@ function auditTrailPlugin(options = {}) {
268
370
  return {
269
371
  name: "auditTrail",
270
372
  apply(repo) {
271
- const AuditModel = getAuditModel(collectionName, ttlDays);
373
+ const AuditModel = getAuditModel(connection ?? repo.Model.db, collectionName, ttlDays);
374
+ if (mode === "transactional") {
375
+ if (repo.hooks.mode === "sync") throw new Error("[auditTrailPlugin] mode: 'transactional' requires the repository's hooks: 'async' mode (the default) — this repository uses hooks: 'sync', whose after-hooks are fire-and-forget and cannot await the audit write.");
376
+ }
377
+ /** Route one entry through the configured delivery mode. */
378
+ const deliver = (entry, context) => {
379
+ if (mode === "transactional") return writeAuditTransactional(AuditModel, entry, context.session, onWriteError);
380
+ writeAuditBestEffort(AuditModel, entry, onWriteError);
381
+ };
272
382
  if (opsSet.has("create")) repo.on("after:create", ({ context, result }) => {
273
383
  if (isSkipped(context)) return;
274
384
  const doc = toPlainObject(result);
275
- const idKey = repo.idField || "_id";
276
- writeAudit(AuditModel, {
385
+ const idKey = getEffectiveIdField(repo, context);
386
+ return deliver({
277
387
  model: context.model || repo.model,
278
388
  operation: "create",
279
389
  documentId: doc?.[idKey],
@@ -281,14 +391,20 @@ function auditTrailPlugin(options = {}) {
281
391
  orgId: context.organizationId,
282
392
  document: trackDocument ? sanitizeDoc(doc, excludeFields) : void 0,
283
393
  metadata: metadata?.(context)
284
- }, onWriteError);
394
+ }, context);
285
395
  });
286
396
  if (opsSet.has("update")) {
287
397
  if (trackChanges) repo.on("before:update", async (context) => {
288
398
  if (isSkipped(context)) return;
289
- if (!context.id) return;
399
+ if (context.id === void 0 || context.id === null) return;
290
400
  try {
291
- const prev = await repo.Model.findById(context.id).lean();
401
+ const filter = {
402
+ [getEffectiveIdField(repo, context)]: context.id,
403
+ ...context.query ?? {}
404
+ };
405
+ const query = repo.Model.findOne(filter);
406
+ if (context.session) query.session(context.session);
407
+ const prev = await query.lean();
292
408
  if (prev) snapshots.set(context, prev);
293
409
  } catch (err) {
294
410
  warn(`[auditTrailPlugin] Failed to snapshot before update: ${err.message}`);
@@ -303,41 +419,41 @@ function auditTrailPlugin(options = {}) {
303
419
  if (prev && context.data) changes = computeChanges(prev, context.data, excludeFields);
304
420
  snapshots.delete(context);
305
421
  }
306
- writeAudit(AuditModel, {
422
+ return deliver({
307
423
  model: context.model || repo.model,
308
424
  operation: "update",
309
- documentId: context.id || doc?.[repo.idField || "_id"],
425
+ documentId: context.id || doc?.[getEffectiveIdField(repo, context)],
310
426
  userId: getUserId(context),
311
427
  orgId: context.organizationId,
312
428
  changes,
313
429
  metadata: metadata?.(context)
314
- }, onWriteError);
430
+ }, context);
315
431
  });
316
432
  }
317
433
  if (opsSet.has("findOneAndUpdate")) repo.on("after:findOneAndUpdate", ({ context, result }) => {
318
434
  if (isSkipped(context)) return;
319
435
  if (!result) return;
320
436
  const doc = result;
321
- const idKey = repo.idField || "_id";
322
- writeAudit(AuditModel, {
437
+ const idKey = getEffectiveIdField(repo, context);
438
+ return deliver({
323
439
  model: context.model || repo.model,
324
440
  operation: "findOneAndUpdate",
325
441
  documentId: doc?.[idKey],
326
442
  userId: getUserId(context),
327
443
  orgId: context.organizationId,
328
444
  metadata: metadata?.(context)
329
- }, onWriteError);
445
+ }, context);
330
446
  });
331
447
  if (opsSet.has("delete")) repo.on("after:delete", ({ context }) => {
332
448
  if (isSkipped(context)) return;
333
- writeAudit(AuditModel, {
449
+ return deliver({
334
450
  model: context.model || repo.model,
335
451
  operation: "delete",
336
452
  documentId: context.id,
337
453
  userId: getUserId(context),
338
454
  orgId: context.organizationId,
339
455
  metadata: metadata?.(context)
340
- }, onWriteError);
456
+ }, context);
341
457
  });
342
458
  if (typeof repo.registerMethod === "function")
343
459
  /**
@@ -379,6 +495,32 @@ function sanitizeDoc(doc, excludeFields) {
379
495
  return result;
380
496
  }
381
497
  /**
498
+ * Create the audit collection and build its indexes BEFORE traffic begins.
499
+ *
500
+ * Call once at boot (after the plugin is registered, before serving
501
+ * requests) — especially with `mode: 'transactional'`: creating the
502
+ * collection or building indexes lazily inside the first transaction
503
+ * forces MongoDB catalog locks and transient `TransientTransactionError`
504
+ * retries. Idempotent; safe to call on every boot.
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * const repo = new Repository(Model, [
509
+ * auditTrailPlugin({ mode: 'transactional', ttlDays: 90 }),
510
+ * ]);
511
+ * await ensureAuditTrailReady(); // collection + indexes exist before traffic
512
+ * ```
513
+ */
514
+ async function ensureAuditTrailReady(options = {}) {
515
+ const model = getAuditModel(options.connection, options.collectionName ?? "audit_trails", options.ttlDays, "reuse-existing");
516
+ try {
517
+ await model.createCollection();
518
+ } catch (err) {
519
+ if (err.codeName !== "NamespaceExists") throw err;
520
+ }
521
+ await model.init();
522
+ }
523
+ /**
382
524
  * Standalone audit trail query utility.
383
525
  * Use this to query audits across all models — e.g., admin dashboards, audit APIs.
384
526
  *
@@ -386,7 +528,10 @@ function sanitizeDoc(doc, excludeFields) {
386
528
  * ```typescript
387
529
  * import { AuditTrailQuery } from '@classytic/mongokit';
388
530
  *
389
- * const auditQuery = new AuditTrailQuery(); // defaults to 'audit_trails' collection
531
+ * const auditQuery = new AuditTrailQuery(); // defaults to 'audit_trails' on mongoose.connection
532
+ *
533
+ * // Named connection (multi-DB apps)
534
+ * const tenantAudits = new AuditTrailQuery({ connection: tenantConnection });
390
535
  *
391
536
  * // All audits for an org
392
537
  * const orgAudits = await auditQuery.query({ orgId: '...' });
@@ -418,8 +563,8 @@ function sanitizeDoc(doc, excludeFields) {
418
563
  */
419
564
  var AuditTrailQuery = class {
420
565
  model;
421
- constructor(collectionName = "audit_trails", ttlDays) {
422
- this.model = getAuditModel(collectionName, ttlDays);
566
+ constructor(options = {}) {
567
+ this.model = getAuditModel(options.connection, options.collectionName ?? "audit_trails", options.ttlDays, "reuse-existing");
423
568
  }
424
569
  /**
425
570
  * Get the underlying Mongoose model for custom queries
@@ -1179,6 +1324,7 @@ function softDeletePlugin(options = {}) {
1179
1324
  apply(repo) {
1180
1325
  try {
1181
1326
  const schemaPaths = repo.Model.schema.paths;
1327
+ if (!schemaPaths[deletedField]) warn(`[softDeletePlugin] Model '${repo.Model.modelName}' has no '${deletedField}' schema path. Soft-delete marks documents by \`$set\`-ing '${deletedField}', but Mongoose strict mode drops updates to undeclared paths — so delete() will SILENTLY no-op (documents are never soft-deleted). Declare it on the schema: { ${deletedField}: { type: Date, default: ${filterMode === "null" ? "null" : "undefined"} } }`);
1182
1328
  for (const [pathName, schemaType] of Object.entries(schemaPaths)) {
1183
1329
  if (pathName === "_id" || pathName === deletedField) continue;
1184
1330
  if (schemaType.options?.unique) warn(`[softDeletePlugin] Field '${pathName}' on model '${repo.Model.modelName}' has a unique index. With soft-delete enabled, deleted documents will block new documents with the same '${pathName}'. Fix: change to a compound partial index — { ${pathName}: 1 }, { unique: true, partialFilterExpression: { ${deletedField}: null } }`);
@@ -1396,4 +1542,4 @@ function timestampPlugin() {
1396
1542
  };
1397
1543
  }
1398
1544
  //#endregion
1399
- export { methodRegistryPlugin as a, getNextSequence as c, cachePlugin as d, batchOperationsPlugin as f, auditLogPlugin as h, multiTenantPlugin as i, prefixedId as l, auditTrailPlugin as m, softDeletePlugin as n, customIdPlugin as o, AuditTrailQuery as p, adminBypass$1 as r, dateSequentialId as s, timestampPlugin as t, sequentialId as u };
1545
+ export { methodRegistryPlugin as a, getNextSequence as c, cachePlugin as d, batchOperationsPlugin as f, auditLogPlugin as g, ensureAuditTrailReady as h, multiTenantPlugin as i, prefixedId as l, auditTrailPlugin as m, softDeletePlugin as n, customIdPlugin as o, AuditTrailQuery as p, adminBypass$1 as r, dateSequentialId as s, timestampPlugin as t, sequentialId as u };
@@ -1,4 +1,5 @@
1
- import { C as FieldPreset, gt as UserContext } from "./types-BtMuhoX5.mjs";
1
+ import { v as UserContext } from "./repository-DCHRWRXw.mjs";
2
+ import { r as FieldPreset } from "./plugin-options-vFufQZMZ.mjs";
2
3
  import mongoose, { Schema } from "mongoose";
3
4
  import { CacheAdapter } from "@classytic/repo-core/cache";
4
5
  import { CrudSchemas, SchemaBuilderOptions } from "@classytic/repo-core/schema";
@@ -139,22 +140,17 @@ declare function filterResponseData<T extends Record<string, unknown>>(data: T |
139
140
  declare function createFieldPreset(config: Partial<FieldPreset>): FieldPreset;
140
141
  //#endregion
141
142
  //#region src/utils/id-resolution.d.ts
142
- /**
143
- * ID Resolution Primitive
144
- *
145
- * Pure functions for detecting a Mongoose schema's _id type and validating
146
- * id values against it. Used by Repository.getById to decide whether to
147
- * run ObjectId validation (for ObjectId schemas) or skip it (for String /
148
- * Number / UUID schemas).
149
- *
150
- * Also usable standalone — Arc controllers, custom repositories, migration
151
- * scripts, or test harnesses can import these directly to make the same
152
- * decision without depending on Repository.
153
- *
154
- * Design: no `this`, no Model, no DB calls. Only reads `schema.paths._id.instance`.
155
- */
156
143
  /** Normalized _id type — covers the common Mongoose _id declarations */
157
144
  type IdType = 'objectid' | 'string' | 'number' | 'uuid' | 'unknown';
145
+ /**
146
+ * Null-tolerant `id → ObjectId` coercion — the single seam for optional
147
+ * reference/audit-stamp fields (`approvedBy`, `submittedBy`, …) where an
148
+ * absent actor is a legitimate `null`, not an error. Invalid non-empty
149
+ * strings still throw (mongoose's own constructor validation) — this helper
150
+ * only absorbs the absent case, never bad input. Hosts were hand-rolling
151
+ * this identically at every approval-hook write; import it instead.
152
+ */
153
+ declare function toObjectId(id: string | null | undefined): mongoose.Types.ObjectId | null;
158
154
  /**
159
155
  * Check if a Mongoose SchemaType `instance` string is an ObjectId variant.
160
156
  * Mongoose 9.x uses `'ObjectId'`, older versions use `'ObjectID'`. This
@@ -324,4 +320,4 @@ declare function buildCrudSchemasFromModel(mongooseModel: mongoose.Model<unknown
324
320
  */
325
321
  declare function toPlain<T>(value: T): T;
326
322
  //#endregion
327
- export { parseDuplicateKeyError as _, configureLogger as a, idVariants as c, createFieldPreset as d, filterResponseData as f, isDuplicateKeyError as g, createError as h, createMemoryCache as i, isObjectIdInstance as l, getMongooseProjection as m, buildCrudSchemasFromModel as n, IdType as o, getFieldsForUser as p, buildCrudSchemasFromMongooseSchema as r, getSchemaIdType as s, toPlain as t, isValidIdForType as u };
323
+ export { isDuplicateKeyError as _, configureLogger as a, idVariants as c, toObjectId as d, createFieldPreset as f, createError as g, getMongooseProjection as h, createMemoryCache as i, isObjectIdInstance as l, getFieldsForUser as m, buildCrudSchemasFromModel as n, IdType as o, filterResponseData as p, buildCrudSchemasFromMongooseSchema as r, getSchemaIdType as s, toPlain as t, isValidIdForType as u, parseDuplicateKeyError as v };
@@ -0,0 +1,163 @@
1
+ import { Document, Model } from "mongoose";
2
+ import { ValidationResult } from "@classytic/repo-core/schema";
3
+ //#region src/types/type-utils.d.ts
4
+ /**
5
+ * Infer document type from a Mongoose Model
6
+ * @example
7
+ * type UserDoc = InferDocument<typeof UserModel>;
8
+ */
9
+ type InferDocument<TModel> = TModel extends Model<infer TDoc> ? TDoc : never;
10
+ /**
11
+ * Infer raw document shape (without Mongoose Document methods)
12
+ * @example
13
+ * type User = InferRawDoc<typeof UserModel>;
14
+ */
15
+ type InferRawDoc<TModel> = TModel extends Model<infer TDoc> ? TDoc extends Document ? Omit<TDoc, keyof Document> : TDoc : never;
16
+ /**
17
+ * Make specific fields optional
18
+ * @example
19
+ * type CreateUser = PartialBy<User, 'createdAt' | 'updatedAt'>;
20
+ */
21
+ type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
22
+ /**
23
+ * Make specific fields required
24
+ * @example
25
+ * type UserWithId = RequiredBy<User, '_id'>;
26
+ */
27
+ type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
28
+ /**
29
+ * Extract keys of type T that have values of type V
30
+ * @example
31
+ * type StringFields = KeysOfType<User, string>; // 'name' | 'email'
32
+ */
33
+ type KeysOfType<T, V> = { [K in keyof T]: T[K] extends V ? K : never; }[keyof T];
34
+ /**
35
+ * Deep partial - makes all nested properties optional
36
+ */
37
+ type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]>; } : T;
38
+ /**
39
+ * Strict object type - prevents excess properties
40
+ * Use with `satisfies` for compile-time validation
41
+ */
42
+ type Strict<T> = T & { [K in Exclude<string, keyof T>]?: never; };
43
+ /**
44
+ * NonNullable fields extractor
45
+ */
46
+ type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]>; };
47
+ /**
48
+ * Create/Update input types from document
49
+ */
50
+ type CreateInput<TDoc> = Omit<TDoc, '_id' | 'createdAt' | 'updatedAt' | '__v'>;
51
+ /**
52
+ * Partial-document update patch — the shape `repo.update(id, data)` accepts
53
+ * for its `data` parameter. A `Partial<TDoc>` minus the fields a caller must
54
+ * never set directly (`_id`, `createdAt`, `__v`).
55
+ *
56
+ * **Renamed from `UpdateInput<TDoc>` in 3.11.0** to eliminate a name
57
+ * collision with `@classytic/repo-core/update`'s `UpdateInput` (the
58
+ * `UpdateSpec | Record | Record[]` union consumed by `updateMany` /
59
+ * `findOneAndUpdate`).
60
+ */
61
+ type UpdatePatch<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>;
62
+ /**
63
+ * Typed Mongo update document — the operator-shape that `findOneAndUpdate`
64
+ * / `updateMany` accept on the wire (`{ $set, $inc, $unset, ... }`).
65
+ *
66
+ * The explicit operator keys give IDE autocomplete + per-operator value
67
+ * typing; the trailing `[op: string]: unknown` index signature lets a
68
+ * caller-built `MongoOperatorUpdate` value assign to `Record<string,
69
+ * unknown>` without a cast — matching repo-core's `UpdateInput` slot
70
+ * (`UpdateSpec | Record<string, unknown> | Record<string, unknown>[]`)
71
+ * and unblocking the historic
72
+ *
73
+ * `normalizeUpdate(update) as unknown as Record<string, unknown>`
74
+ *
75
+ * footgun every domain package eventually wrote.
76
+ *
77
+ * Trade-off: with the index signature, an operator typo (`$st` instead
78
+ * of `$set`) won't fail TypeScript — Mongo would silently drop it. The
79
+ * common operators are listed explicitly so `obj.$set` / `obj.$inc`
80
+ * autocomplete stays useful; the escape hatch covers everything else
81
+ * (`$min`, `$max`, `$rename`, `$pop`, etc.).
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import type { MongoOperatorUpdate } from '@classytic/mongokit';
86
+ *
87
+ * const patch: MongoOperatorUpdate = {
88
+ * $set: { status: 'paid', paidAt: new Date() },
89
+ * $inc: { revision: 1 },
90
+ * };
91
+ * await repo.findOneAndUpdate({ _id: id }, patch); // no cast
92
+ * ```
93
+ */
94
+ interface MongoOperatorUpdate {
95
+ /** Assign top-level or dotted-path values. */
96
+ $set?: Record<string, unknown>;
97
+ /** Set only on insert (upsert path). */
98
+ $setOnInsert?: Record<string, unknown>;
99
+ /** Remove fields (`Mongo` accepts `''`, `1`, or `true`). */
100
+ $unset?: Record<string, '' | 1 | true>;
101
+ /** Atomic numeric increment / decrement. */
102
+ $inc?: Record<string, number>;
103
+ /** Atomic numeric multiplication. */
104
+ $mul?: Record<string, number>;
105
+ /** Push to array (or `{ $each, $position, $slice, $sort }`). */
106
+ $push?: Record<string, unknown>;
107
+ /** Pull matching values from array. */
108
+ $pull?: Record<string, unknown>;
109
+ /** Pull a list of literals from array. */
110
+ $pullAll?: Record<string, unknown[]>;
111
+ /** Push only when not already present. */
112
+ $addToSet?: Record<string, unknown>;
113
+ /** Remove first / last element (`-1` / `1`). */
114
+ $pop?: Record<string, -1 | 1>;
115
+ /** Set only when the new value is smaller / larger. */
116
+ $min?: Record<string, unknown>;
117
+ $max?: Record<string, unknown>;
118
+ /** Rename top-level fields. */
119
+ $rename?: Record<string, string>;
120
+ /** Auto-set Date / Timestamp on this update. */
121
+ $currentDate?: Record<string, true | {
122
+ $type: 'date' | 'timestamp';
123
+ }>;
124
+ /** Bitwise ops on integer fields. */
125
+ $bit?: Record<string, {
126
+ and?: number;
127
+ or?: number;
128
+ xor?: number;
129
+ }>;
130
+ /**
131
+ * Escape hatch for any operator not explicitly listed above (e.g.
132
+ * positional-array operators like `$[<identifier>]` inside a path
133
+ * key). Index signature is what makes the type assign to
134
+ * `Record<string, unknown>` without a cast.
135
+ */
136
+ [op: string]: unknown;
137
+ }
138
+ /**
139
+ * Extract string keys from a document type, with `string` fallback for untyped usage.
140
+ * Provides autocomplete for known fields while still accepting arbitrary strings
141
+ * (e.g. for nested paths like 'address.city').
142
+ */
143
+ type DocField<TDoc> = (keyof TDoc & string) | (string & {});
144
+ /**
145
+ * Union emitted by the validation-chain plugin. Keeps the violation shape
146
+ * from `ValidationResult` without re-exporting the type — consumers that
147
+ * need `ValidationResult`, `CrudSchemas`, `JsonSchema`, `SchemaBuilderOptions`,
148
+ * `FieldRule` or `FieldRules` import them straight from
149
+ * `@classytic/repo-core/schema`.
150
+ */
151
+ type UpdateWithValidationResult<T> = {
152
+ success: true;
153
+ data: T;
154
+ } | {
155
+ success: false;
156
+ error: {
157
+ code: number;
158
+ message: string;
159
+ violations?: ValidationResult['violations'];
160
+ };
161
+ };
162
+ //#endregion
163
+ export { InferRawDoc as a, NonNullableFields as c, Strict as d, UpdatePatch as f, InferDocument as i, PartialBy as l, DeepPartial as n, KeysOfType as o, UpdateWithValidationResult as p, DocField as r, MongoOperatorUpdate as s, CreateInput as t, RequiredBy as u };
@@ -1,2 +1,2 @@
1
- import { a as configureLogger, d as createFieldPreset, f as filterResponseData, h as createError, i as createMemoryCache, l as isObjectIdInstance, m as getMongooseProjection, n as buildCrudSchemasFromModel, o as IdType, p as getFieldsForUser, r as buildCrudSchemasFromMongooseSchema, s as getSchemaIdType, t as toPlain, u as isValidIdForType } from "../to-plain-g8-gsVxZ.mjs";
2
- export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toPlain };
1
+ import { a as configureLogger, d as toObjectId, f as createFieldPreset, g as createError, h as getMongooseProjection, i as createMemoryCache, l as isObjectIdInstance, m as getFieldsForUser, n as buildCrudSchemasFromModel, o as IdType, p as filterResponseData, r as buildCrudSchemasFromMongooseSchema, s as getSchemaIdType, t as toPlain, u as isValidIdForType } from "../to-plain-DkF5px0R.mjs";
2
+ export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toObjectId, toPlain };
@@ -1,7 +1,7 @@
1
- import { i as isValidIdForType, r as isObjectIdInstance, t as getSchemaIdType } from "../id-resolution-P2OzcyRl.mjs";
1
+ import { a as toObjectId, i as isValidIdForType, r as isObjectIdInstance, t as getSchemaIdType } from "../id-resolution-BqDrk0S2.mjs";
2
2
  import { t as configureLogger } from "../logger-K7dPmu2U.mjs";
3
3
  import { t as createError } from "../error-BuByZwKU.mjs";
4
4
  import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "../field-selection-FSQFLpsU.mjs";
5
5
  import { n as createMemoryCache, t as toPlain } from "../to-plain-vksRrLtc.mjs";
6
- import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-CVnrKFDq.mjs";
7
- export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toPlain };
6
+ import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-CByt1VhQ.mjs";
7
+ export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType, toObjectId, toPlain };