@classytic/mongokit 3.24.0 → 3.25.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.
- package/README.md +11 -2
- package/dist/{PaginationEngine-BnZ24ehC.d.mts → PaginationEngine-CBS1XGzp.d.mts} +2 -1
- package/dist/{Repository-ClOraFjT.mjs → Repository-KKR8jxfD.mjs} +70 -41
- package/dist/actions/index.d.mts +1 -1
- package/dist/adapter/index.d.mts +1 -1
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-BWgJ_Jdc.mjs → adapter-CLvUHiTW.mjs} +1 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/core-62oo3N1C.d.mts +50 -0
- package/dist/{create-repository-CVupgeRG.mjs → create-repository-DA5VJh-3.mjs} +2 -2
- package/dist/{create-repository-Cn1LkvL7.d.mts → create-repository-HjMD9h9_.d.mts} +18 -5
- package/dist/{id-resolution-P2OzcyRl.mjs → id-resolution-BqDrk0S2.mjs} +12 -1
- package/dist/{index-DLQ6pjal.d.mts → index-DceInCTX.d.mts} +3 -1
- package/dist/index.d.mts +53 -213
- package/dist/index.mjs +1071 -922
- package/dist/{mongooseToJsonSchema-CVnrKFDq.mjs → mongooseToJsonSchema-CByt1VhQ.mjs} +1 -1
- package/dist/{multi-tenant.plugin-BjXXuc_g.d.mts → multi-tenant.plugin-DVyVGREb.d.mts} +117 -18
- package/dist/operations-C8IcCHF9.d.mts +463 -0
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination-jSUdrACK.d.mts +158 -0
- package/dist/plugin-options-vFufQZMZ.d.mts +125 -0
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +3 -3
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/geo.d.mts +1 -1
- package/dist/repository-DCHRWRXw.d.mts +511 -0
- package/dist/testkit/index.d.mts +1 -1
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-BhSX0Lu_.mjs → timestamp.plugin-B8252GuQ.mjs} +192 -46
- package/dist/{to-plain-g8-gsVxZ.d.mts → to-plain-DkF5px0R.d.mts} +12 -16
- package/dist/type-utils-CLzRFtIA.d.mts +163 -0
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +3 -3
- package/dist/{validation-chain.plugin-Dtt4tO7R.mjs → validation-chain.plugin-C3Vwp-Rk.mjs} +1 -1
- package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-DxD15h-2.d.mts} +3 -1
- package/package.json +5 -3
- 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-
|
|
1
|
+
import { l as OP_REGISTRY, s as ALL_OPERATIONS, t as HOOK_PRIORITY } from "./Repository-KKR8jxfD.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
|
-
*
|
|
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
|
-
* -
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
276
|
-
|
|
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
|
-
},
|
|
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 (
|
|
399
|
+
if (context.id === void 0 || context.id === null) return;
|
|
290
400
|
try {
|
|
291
|
-
const
|
|
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
|
-
|
|
422
|
+
return deliver({
|
|
307
423
|
model: context.model || repo.model,
|
|
308
424
|
operation: "update",
|
|
309
|
-
documentId: context.id || doc?.[repo
|
|
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
|
-
},
|
|
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
|
|
322
|
-
|
|
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
|
-
},
|
|
445
|
+
}, context);
|
|
330
446
|
});
|
|
331
447
|
if (opsSet.has("delete")) repo.on("after:delete", ({ context }) => {
|
|
332
448
|
if (isSkipped(context)) return;
|
|
333
|
-
|
|
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
|
-
},
|
|
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'
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 };
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as configureLogger, d as
|
|
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 };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as isValidIdForType, r as isObjectIdInstance, t as getSchemaIdType } from "../id-resolution-
|
|
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-
|
|
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-
|
|
1
|
+
import { l as OP_REGISTRY, s as ALL_OPERATIONS } from "./Repository-KKR8jxfD.mjs";
|
|
2
2
|
import { _ as upsert } from "./update-1Lq9OMD2.mjs";
|
|
3
3
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
4
4
|
import { t as createError } from "./error-BuByZwKU.mjs";
|
package/dist/{validation-chain.plugin-CFJbr964.d.mts → validation-chain.plugin-DxD15h-2.d.mts}
RENAMED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as PopulateSpec, i as ObjectId, l as SortSpec, s as SelectSpec } from "./core-62oo3N1C.mjs";
|
|
2
|
+
import { h as RepositoryInstance, l as Plugin, p as RepositoryContext } from "./repository-DCHRWRXw.mjs";
|
|
3
|
+
import { c as ValidationChainOptions, i as Logger, l as ValidatorDefinition, o as SoftDeleteOptions, r as FieldPreset, t as CascadeOptions } from "./plugin-options-vFufQZMZ.mjs";
|
|
2
4
|
import { BulkWriteResult } from "@classytic/repo-core/repository";
|
|
3
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
6
|
import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/mongokit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.0",
|
|
4
4
|
"description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -139,8 +139,10 @@
|
|
|
139
139
|
"lint:package": "publint && attw --pack . --profile esm-only",
|
|
140
140
|
"format": "biome format src/ --write",
|
|
141
141
|
"check": "biome ci src/ --diagnostic-level=error",
|
|
142
|
+
"check:dead-code": "knip",
|
|
143
|
+
"ci": "npm run check && npm run typecheck && npm run typecheck:tests && npm run build && npm run lint:package && npm run check:dead-code && npm test",
|
|
142
144
|
"push": "classytic-push",
|
|
143
|
-
"prepublishOnly": "npm run
|
|
145
|
+
"prepublishOnly": "npm run ci",
|
|
144
146
|
"publish:dry": "npm publish --dry-run --access public",
|
|
145
147
|
"publish:npm": "npm publish --access public",
|
|
146
148
|
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
@@ -168,7 +170,7 @@
|
|
|
168
170
|
"mongoose": "^9.4.1",
|
|
169
171
|
"publint": "^0.3.18",
|
|
170
172
|
"tsdown": "^0.22.5",
|
|
171
|
-
"typescript": "
|
|
173
|
+
"typescript": "~5.9.0",
|
|
172
174
|
"vitest": "^4.1.4"
|
|
173
175
|
},
|
|
174
176
|
"overrides": {
|