@classytic/mongokit 3.18.0 → 3.20.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 +66 -0
- package/dist/{PaginationEngine-BcvaXBQ-.mjs → PaginationEngine-BT1ywu9k.mjs} +327 -6
- package/dist/PaginationEngine-DYpOD2wY.d.mts +102 -0
- package/dist/{Repository-B01VW_fU.mjs → Repository-dbbgOGjK.mjs} +85 -11
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +2 -2
- package/dist/adapter/index.d.mts +10 -2
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-DYEMkLwK.mjs → adapter-CW1EL0fF.mjs} +10 -1
- package/dist/ai/index.d.mts +1 -2
- package/dist/ai/index.mjs +3 -2
- package/dist/better-auth/index.d.mts +0 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/create-repository-0TkQhcv-.d.mts +1757 -0
- package/dist/create-repository-CJATiAKX.mjs +29 -0
- package/dist/{id-resolution-CnN0YA8t.mjs → id-resolution-P2OzcyRl.mjs} +10 -5
- package/dist/{index-fi91ycQc.d.mts → index-Cg3SWugF.d.mts} +1 -3
- package/dist/index.d.mts +11 -1666
- package/dist/index.mjs +51 -37
- package/dist/lock/index.d.mts +0 -1
- package/dist/{mongooseToJsonSchema-BdQJU39w.d.mts → mongooseToJsonSchema-D2CyAMIC.d.mts} +9 -5
- package/dist/{mongooseToJsonSchema-CBEQmeEa.mjs → mongooseToJsonSchema-khdzVo8x.mjs} +1 -1
- package/dist/multi-tenant.plugin-CTsWz0XK.d.mts +486 -0
- package/dist/pagination/PaginationEngine.d.mts +1 -102
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +2 -1
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/geo.d.mts +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/testkit/index.d.mts +128 -0
- package/dist/testkit/index.mjs +139 -0
- package/dist/{validation-chain.plugin-DJtC0KJ-.mjs → timestamp.plugin-nG9Z1JEk.mjs} +5 -1056
- package/dist/{types-CU3SEDbD.d.mts → types-DNFMofHy.d.mts} +51 -22
- package/dist/{update-CvCMv5Cl.mjs → update-1Lq9OMD2.mjs} +5 -5
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +5 -5
- package/dist/validation-chain.plugin-Dn_wZYLA.mjs +1056 -0
- package/dist/{validation-chain.plugin-BpMfy18N.d.mts → validation-chain.plugin-buy3uvyk.d.mts} +2 -487
- package/package.json +16 -7
- package/dist/cursor-_SbWVAn-.mjs +0 -254
- package/dist/sort-Ca1BPfvE.mjs +0 -72
- /package/dist/{coercion-TRMmR49h.d.mts → coercion-BjdPX3YF.d.mts} +0 -0
- /package/dist/{error-BTeepSNL.mjs → error-BuByZwKU.mjs} +0 -0
- /package/dist/{field-selection-CQW3WBiK.mjs → field-selection-FSQFLpsU.mjs} +0 -0
- /package/dist/{indexes-CgOF7Wjd.d.mts → indexes-DIjMpYES.d.mts} +0 -0
- /package/dist/{logger-B8wf-Mtb.mjs → logger-K7dPmu2U.mjs} +0 -0
- /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { n as idVariants } from "./id-resolution-
|
|
2
|
-
import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-
|
|
3
|
-
import { b as LookupBuilder, x as compileFilterToMongo } from "./update-
|
|
4
|
-
import { n as warn, t as configureLogger } from "./logger-
|
|
5
|
-
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-
|
|
1
|
+
import { n as idVariants } from "./id-resolution-P2OzcyRl.mjs";
|
|
2
|
+
import { a as withTransaction, c as MUTATING_OPERATIONS, d as operationsByPolicyKey, f as MONGOKIT_CAPABILITIES, i as isTransactionUnsupported, l as OP_REGISTRY, n as Repository, o as AggregationBuilder, p as createMongoPurgePort, r as batchTransaction, s as ALL_OPERATIONS, t as HOOK_PRIORITY, u as READ_OPERATIONS } from "./Repository-dbbgOGjK.mjs";
|
|
3
|
+
import { b as LookupBuilder, x as compileFilterToMongo } from "./update-1Lq9OMD2.mjs";
|
|
4
|
+
import { n as warn, t as configureLogger } from "./logger-K7dPmu2U.mjs";
|
|
5
|
+
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
6
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
7
|
+
import { a as methodRegistryPlugin, c as getNextSequence, d as cachePlugin, f as batchOperationsPlugin, h as auditLogPlugin, i as multiTenantPlugin, l as prefixedId, m as auditTrailPlugin, n as softDeletePlugin, o as customIdPlugin, p as AuditTrailQuery, r as adminBypass, s as dateSequentialId, t as timestampPlugin, u as sequentialId } from "./timestamp.plugin-nG9Z1JEk.mjs";
|
|
8
|
+
import { t as PaginationEngine } from "./PaginationEngine-BT1ywu9k.mjs";
|
|
9
9
|
import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { t as createRepository } from "./create-repository-CJATiAKX.mjs";
|
|
11
|
+
import { a as uniqueField, c as subdocumentPlugin, d as leasePlugin, f as fieldFilterPlugin, h as aggregateHelpersPlugin, i as requireField, l as observabilityPlugin, m as cascadePlugin, n as blockIf, o as validationChainPlugin, p as elasticSearchPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as mongoOperationsPlugin } from "./validation-chain.plugin-Dn_wZYLA.mjs";
|
|
12
|
+
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
|
|
11
13
|
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
12
14
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
13
|
-
import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-
|
|
15
|
+
import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-khdzVo8x.mjs";
|
|
14
16
|
import { runChunkedPurge } from "@classytic/repo-core/repository";
|
|
15
17
|
import { isControlParam } from "@classytic/repo-core/query-parser";
|
|
16
18
|
//#region src/actions/cascade-references.ts
|
|
@@ -142,32 +144,6 @@ async function cascadePurgeReferences(options) {
|
|
|
142
144
|
return out;
|
|
143
145
|
}
|
|
144
146
|
//#endregion
|
|
145
|
-
//#region src/create-repository.ts
|
|
146
|
-
function isLoggerAudit(value) {
|
|
147
|
-
const v = value;
|
|
148
|
-
return typeof v.info === "function" || typeof v.warn === "function" || typeof v.error === "function" || typeof v.debug === "function";
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Build a `Repository` from a declarative config. See module JSDoc for
|
|
152
|
-
* ordering guarantees and an example.
|
|
153
|
-
*/
|
|
154
|
-
function createRepository(model, config = {}) {
|
|
155
|
-
const { tenant, softDelete, timestamps, batch, cache, audit, customId, plugins = [], pagination = {}, ...repositoryOptions } = config;
|
|
156
|
-
const stack = [methodRegistryPlugin()];
|
|
157
|
-
if (tenant) stack.push(multiTenantPlugin(tenant));
|
|
158
|
-
if (softDelete) stack.push(softDeletePlugin(softDelete === true ? {} : softDelete));
|
|
159
|
-
if (timestamps) stack.push(timestampPlugin());
|
|
160
|
-
if (customId) stack.push(customIdPlugin(customId));
|
|
161
|
-
if (cache) stack.push(cachePlugin(cache));
|
|
162
|
-
if (audit) stack.push(isLoggerAudit(audit) ? auditLogPlugin(audit) : auditTrailPlugin(audit));
|
|
163
|
-
if (batch) stack.push(batchOperationsPlugin());
|
|
164
|
-
stack.push(...plugins);
|
|
165
|
-
return new Repository(model, stack, pagination, {
|
|
166
|
-
pluginOrderChecks: repositoryOptions.pluginOrderChecks ?? "throw",
|
|
167
|
-
...repositoryOptions
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
//#endregion
|
|
171
147
|
//#region src/query/QueryParser.ts
|
|
172
148
|
/**
|
|
173
149
|
* Modern Query Parser - URL to MongoDB Query Transpiler
|
|
@@ -250,7 +226,7 @@ function createRepository(model, config = {}) {
|
|
|
250
226
|
* without this guard. Kept as a Set for O(1) lookup; values mirror the
|
|
251
227
|
* reserved-key list at the top of `_parseFilters`.
|
|
252
228
|
*/
|
|
253
|
-
const RESERVED_PAGINATION_KEYS = new Set([
|
|
229
|
+
const RESERVED_PAGINATION_KEYS = /* @__PURE__ */ new Set([
|
|
254
230
|
"page",
|
|
255
231
|
"limit",
|
|
256
232
|
"sort",
|
|
@@ -1067,7 +1043,7 @@ var QueryParser = class {
|
|
|
1067
1043
|
return {};
|
|
1068
1044
|
}
|
|
1069
1045
|
const sanitized = {};
|
|
1070
|
-
const logicalArrayOps = new Set([
|
|
1046
|
+
const logicalArrayOps = /* @__PURE__ */ new Set([
|
|
1071
1047
|
"$or",
|
|
1072
1048
|
"$and",
|
|
1073
1049
|
"$nor"
|
|
@@ -1445,6 +1421,44 @@ function systemContext(extra) {
|
|
|
1445
1421
|
}
|
|
1446
1422
|
//#endregion
|
|
1447
1423
|
//#region src/index.ts
|
|
1424
|
+
/**
|
|
1425
|
+
* MongoKit - Event-driven repository pattern for MongoDB
|
|
1426
|
+
*
|
|
1427
|
+
* Production-grade MongoDB repositories with zero dependencies -
|
|
1428
|
+
* smart pagination, events, and plugins.
|
|
1429
|
+
*
|
|
1430
|
+
* @module @classytic/mongokit
|
|
1431
|
+
* @author Classytic (https://github.com/classytic)
|
|
1432
|
+
* @license MIT
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* ```typescript
|
|
1436
|
+
* import { createRepository } from '@classytic/mongokit';
|
|
1437
|
+
*
|
|
1438
|
+
* // Create repository with declarative config (plugins composed in the
|
|
1439
|
+
* // canonical safe order automatically)
|
|
1440
|
+
* const userRepo = createRepository(UserModel, {
|
|
1441
|
+
* timestamps: true,
|
|
1442
|
+
* softDelete: true,
|
|
1443
|
+
* });
|
|
1444
|
+
*
|
|
1445
|
+
* // Create
|
|
1446
|
+
* const user = await userRepo.create({ name: 'John', email: 'john@example.com' });
|
|
1447
|
+
*
|
|
1448
|
+
* // Read with pagination (auto-detects offset vs keyset)
|
|
1449
|
+
* const users = await userRepo.getAll({ page: 1, limit: 20 });
|
|
1450
|
+
*
|
|
1451
|
+
* // Keyset pagination for infinite scroll
|
|
1452
|
+
* const stream = await userRepo.getAll({ sort: { createdAt: -1 }, limit: 50 });
|
|
1453
|
+
* const nextStream = await userRepo.getAll({ after: stream.next, sort: { createdAt: -1 } });
|
|
1454
|
+
*
|
|
1455
|
+
* // Update
|
|
1456
|
+
* await userRepo.update(user._id, { name: 'John Doe' });
|
|
1457
|
+
*
|
|
1458
|
+
* // Delete
|
|
1459
|
+
* await userRepo.delete(user._id);
|
|
1460
|
+
* ```
|
|
1461
|
+
*/
|
|
1448
1462
|
var src_default = Repository;
|
|
1449
1463
|
//#endregion
|
|
1450
1464
|
export { ALL_OPERATIONS, AggregationBuilder, AuditTrailQuery, HOOK_PRIORITY, LookupBuilder, MONGOKIT_CAPABILITIES, MUTATING_OPERATIONS, OP_REGISTRY, PaginationEngine, QueryParser, READ_OPERATIONS, Repository, actions_exports as actions, adminBypass, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, batchTransaction, blockIf, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, cachePlugin, cascadePlugin, cascadePurgeReferences, compileFilterToMongo, configureLogger, createError, createFieldPreset, createMemoryCache, createOptionsExtractor, createRepository, createTenantContext, customIdPlugin, dateSequentialId, src_default as default, elasticSearchPlugin, fieldFilterPlugin, filterResponseData, getFieldsForUser, getMongooseProjection, getNextSequence, idVariants, immutableField, isDuplicateKeyError, isTransactionUnsupported, leasePlugin, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, operationsByPolicyKey, parseDuplicateKeyError, prefixedId, repoOptionsFromCtx, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, systemContext, timestampPlugin, uniqueField, validationChainPlugin, withTransaction };
|
package/dist/lock/index.d.mts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { C as FieldPreset, gt as UserContext } from "./types-
|
|
1
|
+
import { C as FieldPreset, gt as UserContext } from "./types-DNFMofHy.mjs";
|
|
2
2
|
import mongoose, { Schema } from "mongoose";
|
|
3
3
|
import { CacheAdapter } from "@classytic/repo-core/cache";
|
|
4
4
|
import { CrudSchemas, SchemaBuilderOptions } from "@classytic/repo-core/schema";
|
|
5
5
|
import { HttpError } from "@classytic/repo-core/errors";
|
|
6
|
-
|
|
7
6
|
//#region src/utils/error.d.ts
|
|
8
7
|
/** Optional structured fields for createError. */
|
|
9
8
|
interface CreateErrorOptions {
|
|
@@ -170,9 +169,10 @@ interface SchemaLike {
|
|
|
170
169
|
}>;
|
|
171
170
|
}
|
|
172
171
|
/**
|
|
173
|
-
* Detect
|
|
172
|
+
* Detect an id path's type from a Mongoose schema. Defaults to the `_id`
|
|
173
|
+
* path; pass `field` to resolve a custom id field (Repository's `idField`).
|
|
174
174
|
*
|
|
175
|
-
*
|
|
175
|
+
* For `_id`, returns 'objectid' as the safe default when:
|
|
176
176
|
* - No schema is provided (mock models, test harnesses)
|
|
177
177
|
* - The schema has no `paths._id` (auto-generated ObjectId)
|
|
178
178
|
* - The instance string is unrecognized
|
|
@@ -180,8 +180,12 @@ interface SchemaLike {
|
|
|
180
180
|
* This safe-default means existing callers that don't set _id explicitly
|
|
181
181
|
* get the same ObjectId validation they always had — the behavioral change
|
|
182
182
|
* is strictly opt-in via schema declaration.
|
|
183
|
+
*
|
|
184
|
+
* For a custom `field` with no resolvable path, returns 'unknown' instead —
|
|
185
|
+
* custom id fields carry no ObjectId convention, so validation stays
|
|
186
|
+
* permissive and the DB remains the authority.
|
|
183
187
|
*/
|
|
184
|
-
declare function getSchemaIdType(schema?: SchemaLike | null): IdType;
|
|
188
|
+
declare function getSchemaIdType(schema?: SchemaLike | null, field?: string): IdType;
|
|
185
189
|
/**
|
|
186
190
|
* Validate an id value against a known _id type — WITHOUT hitting the DB.
|
|
187
191
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as isObjectIdInstance } from "./id-resolution-
|
|
1
|
+
import { r as isObjectIdInstance } from "./id-resolution-P2OzcyRl.mjs";
|
|
2
2
|
import mongoose from "mongoose";
|
|
3
3
|
import { applyFieldRules, collectFieldsToOmit } from "@classytic/repo-core/schema";
|
|
4
4
|
//#region src/utils/memory-cache.ts
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { B as ObjectId, G as Plugin, Q as RepositoryContext } from "./types-DNFMofHy.mjs";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
|
|
4
|
+
import { TenantConfig } from "@classytic/repo-core/tenant";
|
|
5
|
+
//#region src/plugins/audit-trail.plugin.d.ts
|
|
6
|
+
interface AuditTrailOptions {
|
|
7
|
+
/** Operations to track (default: ['create', 'update', 'delete']) */
|
|
8
|
+
operations?: AuditOperation[];
|
|
9
|
+
/** Store field-level before/after diff on updates (default: true) */
|
|
10
|
+
trackChanges?: boolean;
|
|
11
|
+
/** Store full document snapshot on create (default: false — can be heavy) */
|
|
12
|
+
trackDocument?: boolean;
|
|
13
|
+
/** Auto-purge after N days via MongoDB TTL index (default: undefined — keep forever) */
|
|
14
|
+
ttlDays?: number;
|
|
15
|
+
/** MongoDB collection name (default: 'audit_trails') */
|
|
16
|
+
collectionName?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Extract custom metadata from the repository context.
|
|
19
|
+
* Returned object is stored on the audit entry as `metadata`.
|
|
20
|
+
*/
|
|
21
|
+
metadata?: (context: RepositoryContext) => Record<string, unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Fields to exclude from change tracking (e.g., passwords, tokens).
|
|
24
|
+
* These fields are redacted in the `changes` diff.
|
|
25
|
+
*/
|
|
26
|
+
excludeFields?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Custom callback fired when an audit write fails. Receives the error
|
|
29
|
+
* and the entry that failed to land. Default: `console.warn` only.
|
|
30
|
+
*
|
|
31
|
+
* **Use this for compliance-grade audit trails (SOC 2 / PCI / HIPAA)
|
|
32
|
+
* where missing entries must be surfaced upstream** — e.g. forward to
|
|
33
|
+
* a dead-letter queue, page on-call, or short-circuit the request.
|
|
34
|
+
* The default `warn` log is appropriate for development and
|
|
35
|
+
* eventually-consistent observability use cases but is invisible to
|
|
36
|
+
* compliance review.
|
|
37
|
+
*
|
|
38
|
+
* The callback runs on the same fire-and-forget microtask that wrote
|
|
39
|
+
* the entry, so throwing from here will surface as an unhandled
|
|
40
|
+
* rejection — handle (or rethrow into your error reporter) inside.
|
|
41
|
+
*/
|
|
42
|
+
onWriteError?: (error: Error, entry: Omit<AuditEntry, 'timestamp'>) => void;
|
|
43
|
+
}
|
|
44
|
+
type AuditOperation = 'create' | 'update' | 'delete' | 'findOneAndUpdate';
|
|
45
|
+
interface AuditEntry {
|
|
46
|
+
model: string;
|
|
47
|
+
operation: AuditOperation;
|
|
48
|
+
documentId: unknown;
|
|
49
|
+
userId?: unknown;
|
|
50
|
+
orgId?: unknown;
|
|
51
|
+
changes?: Record<string, {
|
|
52
|
+
from: unknown;
|
|
53
|
+
to: unknown;
|
|
54
|
+
}>;
|
|
55
|
+
document?: Record<string, unknown>;
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
timestamp: Date;
|
|
58
|
+
}
|
|
59
|
+
declare function auditTrailPlugin(options?: AuditTrailOptions): Plugin;
|
|
60
|
+
interface AuditQueryOptions {
|
|
61
|
+
model?: string;
|
|
62
|
+
documentId?: string | ObjectId;
|
|
63
|
+
userId?: string | ObjectId;
|
|
64
|
+
orgId?: string | ObjectId;
|
|
65
|
+
operation?: AuditOperation;
|
|
66
|
+
from?: Date;
|
|
67
|
+
to?: Date;
|
|
68
|
+
page?: number;
|
|
69
|
+
limit?: number;
|
|
70
|
+
}
|
|
71
|
+
interface AuditQueryResult {
|
|
72
|
+
data: AuditEntry[];
|
|
73
|
+
page: number;
|
|
74
|
+
limit: number;
|
|
75
|
+
total: number;
|
|
76
|
+
pages: number;
|
|
77
|
+
hasNext: boolean;
|
|
78
|
+
hasPrev: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Standalone audit trail query utility.
|
|
82
|
+
* Use this to query audits across all models — e.g., admin dashboards, audit APIs.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import { AuditTrailQuery } from '@classytic/mongokit';
|
|
87
|
+
*
|
|
88
|
+
* const auditQuery = new AuditTrailQuery(); // defaults to 'audit_trails' collection
|
|
89
|
+
*
|
|
90
|
+
* // All audits for an org
|
|
91
|
+
* const orgAudits = await auditQuery.query({ orgId: '...' });
|
|
92
|
+
*
|
|
93
|
+
* // All updates by a user
|
|
94
|
+
* const userUpdates = await auditQuery.query({
|
|
95
|
+
* userId: '...',
|
|
96
|
+
* operation: 'update',
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* // All audits for a specific document
|
|
100
|
+
* const docHistory = await auditQuery.query({
|
|
101
|
+
* model: 'Job',
|
|
102
|
+
* documentId: '...',
|
|
103
|
+
* });
|
|
104
|
+
*
|
|
105
|
+
* // Date range
|
|
106
|
+
* const recent = await auditQuery.query({
|
|
107
|
+
* from: new Date('2025-01-01'),
|
|
108
|
+
* to: new Date(),
|
|
109
|
+
* page: 1,
|
|
110
|
+
* limit: 50,
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // Direct model access for custom queries
|
|
114
|
+
* const model = auditQuery.getModel();
|
|
115
|
+
* const count = await model.countDocuments({ operation: 'delete' });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare class AuditTrailQuery {
|
|
119
|
+
private model;
|
|
120
|
+
constructor(collectionName?: string, ttlDays?: number);
|
|
121
|
+
/**
|
|
122
|
+
* Get the underlying Mongoose model for custom queries
|
|
123
|
+
*/
|
|
124
|
+
getModel(): mongoose.Model<AuditEntry>;
|
|
125
|
+
/**
|
|
126
|
+
* Query audit entries with filters and pagination
|
|
127
|
+
*/
|
|
128
|
+
query(options?: AuditQueryOptions): Promise<AuditQueryResult>;
|
|
129
|
+
/**
|
|
130
|
+
* Get audit trail for a specific document
|
|
131
|
+
*/
|
|
132
|
+
getDocumentTrail(model: string, documentId: string | ObjectId, options?: {
|
|
133
|
+
page?: number;
|
|
134
|
+
limit?: number;
|
|
135
|
+
operation?: AuditOperation;
|
|
136
|
+
}): Promise<AuditQueryResult>;
|
|
137
|
+
/**
|
|
138
|
+
* Get all audits for a user
|
|
139
|
+
*/
|
|
140
|
+
getUserTrail(userId: string | ObjectId, options?: {
|
|
141
|
+
page?: number;
|
|
142
|
+
limit?: number;
|
|
143
|
+
operation?: AuditOperation;
|
|
144
|
+
orgId?: string | ObjectId;
|
|
145
|
+
}): Promise<AuditQueryResult>;
|
|
146
|
+
/**
|
|
147
|
+
* Get all audits for an organization
|
|
148
|
+
*/
|
|
149
|
+
getOrgTrail(orgId: string | ObjectId, options?: {
|
|
150
|
+
page?: number;
|
|
151
|
+
limit?: number;
|
|
152
|
+
operation?: AuditOperation;
|
|
153
|
+
model?: string;
|
|
154
|
+
}): Promise<AuditQueryResult>;
|
|
155
|
+
}
|
|
156
|
+
interface AuditTrailMethods {
|
|
157
|
+
/**
|
|
158
|
+
* Get paginated audit trail for a document
|
|
159
|
+
*/
|
|
160
|
+
getAuditTrail(documentId: string | ObjectId, options?: {
|
|
161
|
+
page?: number;
|
|
162
|
+
limit?: number;
|
|
163
|
+
operation?: AuditOperation;
|
|
164
|
+
}): Promise<AuditQueryResult>;
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/plugins/custom-id.plugin.d.ts
|
|
168
|
+
/**
|
|
169
|
+
* Generator function that produces a unique ID.
|
|
170
|
+
* Receives the full repository context for conditional logic.
|
|
171
|
+
*/
|
|
172
|
+
type IdGenerator = (context: RepositoryContext) => string | Promise<string>;
|
|
173
|
+
interface CustomIdOptions {
|
|
174
|
+
/** Field to store the custom ID (default: 'customId') */
|
|
175
|
+
field?: string;
|
|
176
|
+
/** Function to generate the ID. Can be async. */
|
|
177
|
+
generator: IdGenerator;
|
|
178
|
+
/** Only generate if the field is missing/empty (default: true) */
|
|
179
|
+
generateOnlyIfEmpty?: boolean;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Atomically increment and return the next sequence value for a given key.
|
|
183
|
+
* Uses `findOneAndUpdate` with `upsert` + `$inc` — fully atomic even under
|
|
184
|
+
* heavy concurrency.
|
|
185
|
+
*
|
|
186
|
+
* @param counterKey - Unique key identifying this counter (e.g., "Invoice" or "Invoice:2026-02")
|
|
187
|
+
* @param increment - Value to increment by (default: 1)
|
|
188
|
+
* @param connection - Mongoose connection to use (defaults to the default connection)
|
|
189
|
+
* @param session - Optional client session. When provided, the counter bump
|
|
190
|
+
* participates in the caller's transaction — if the surrounding transaction
|
|
191
|
+
* aborts, the counter does NOT advance. Without a session, the bump commits
|
|
192
|
+
* immediately and is not rolled back if a sibling write fails.
|
|
193
|
+
* @returns The next sequence number (after increment)
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* const seq = await getNextSequence('invoices');
|
|
197
|
+
* // First call → 1, second → 2, ...
|
|
198
|
+
*
|
|
199
|
+
* @example Batch increment for createMany
|
|
200
|
+
* const startSeq = await getNextSequence('invoices', 5);
|
|
201
|
+
* // If current was 10, returns 15 (you use 11, 12, 13, 14, 15)
|
|
202
|
+
*
|
|
203
|
+
* @example Transactional counter bump
|
|
204
|
+
* await withTransaction(conn, async (session) => {
|
|
205
|
+
* const seq = await getNextSequence('invoices', 1, conn, session);
|
|
206
|
+
* await invoiceRepo.create({ invoiceNumber: `INV-${seq}` }, { session });
|
|
207
|
+
* // if this throws, the counter is rolled back with the insert
|
|
208
|
+
* });
|
|
209
|
+
*/
|
|
210
|
+
declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
|
|
211
|
+
interface SequentialIdOptions {
|
|
212
|
+
/** Prefix string (e.g., 'INV', 'ORD') */
|
|
213
|
+
prefix: string;
|
|
214
|
+
/** Mongoose model — used to derive the counter key from model name */
|
|
215
|
+
model: mongoose.Model<any>;
|
|
216
|
+
/** Number of digits to pad to (default: 4 → "0001") */
|
|
217
|
+
padding?: number;
|
|
218
|
+
/** Separator between prefix and number (default: '-') */
|
|
219
|
+
separator?: string;
|
|
220
|
+
/** Custom counter key override (default: model.modelName) */
|
|
221
|
+
counterKey?: string;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Generator: Simple sequential counter.
|
|
225
|
+
* Produces IDs like `INV-0001`, `INV-0002`, etc.
|
|
226
|
+
*
|
|
227
|
+
* Uses atomic MongoDB counters — safe under concurrency.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* customIdPlugin({
|
|
232
|
+
* field: 'invoiceNumber',
|
|
233
|
+
* generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
|
|
234
|
+
* })
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
declare function sequentialId(options: SequentialIdOptions): IdGenerator;
|
|
238
|
+
interface DateSequentialIdOptions {
|
|
239
|
+
/** Prefix string (e.g., 'BILL', 'INV') */
|
|
240
|
+
prefix: string;
|
|
241
|
+
/** Mongoose model — used to derive the counter key */
|
|
242
|
+
model: mongoose.Model<any>;
|
|
243
|
+
/**
|
|
244
|
+
* Partition granularity — counter resets each period.
|
|
245
|
+
* - 'yearly' → BILL-2026-0001, resets every January
|
|
246
|
+
* - 'monthly' → BILL-2026-02-0001, resets every month
|
|
247
|
+
* - 'daily' → BILL-2026-02-20-0001, resets every day
|
|
248
|
+
*/
|
|
249
|
+
partition?: 'yearly' | 'monthly' | 'daily';
|
|
250
|
+
/** Number of digits to pad to (default: 4) */
|
|
251
|
+
padding?: number;
|
|
252
|
+
/** Separator (default: '-') */
|
|
253
|
+
separator?: string;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Generator: Date-partitioned sequential counter.
|
|
257
|
+
* Counter resets per period — great for invoice/bill numbering.
|
|
258
|
+
*
|
|
259
|
+
* Produces IDs like:
|
|
260
|
+
* - yearly: `BILL-2026-0001`
|
|
261
|
+
* - monthly: `BILL-2026-02-0001`
|
|
262
|
+
* - daily: `BILL-2026-02-20-0001`
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* ```typescript
|
|
266
|
+
* customIdPlugin({
|
|
267
|
+
* field: 'billNumber',
|
|
268
|
+
* generator: dateSequentialId({
|
|
269
|
+
* prefix: 'BILL',
|
|
270
|
+
* model: BillModel,
|
|
271
|
+
* partition: 'monthly',
|
|
272
|
+
* }),
|
|
273
|
+
* })
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
|
|
277
|
+
interface PrefixedIdOptions {
|
|
278
|
+
/** Prefix string (e.g., 'USR', 'TXN') */
|
|
279
|
+
prefix: string;
|
|
280
|
+
/** Separator (default: '_') */
|
|
281
|
+
separator?: string;
|
|
282
|
+
/** Length of the random suffix (default: 12) */
|
|
283
|
+
length?: number;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Generator: Prefix + random alphanumeric suffix.
|
|
287
|
+
* Does NOT require a database round-trip — purely in-memory.
|
|
288
|
+
*
|
|
289
|
+
* Produces IDs like: `USR_a7b3xk9m2p1q`
|
|
290
|
+
*
|
|
291
|
+
* Good for: user-facing IDs where ordering doesn't matter.
|
|
292
|
+
* Not suitable for sequential numbering.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```typescript
|
|
296
|
+
* customIdPlugin({
|
|
297
|
+
* field: 'publicId',
|
|
298
|
+
* generator: prefixedId({ prefix: 'USR', length: 10 }),
|
|
299
|
+
* })
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
declare function prefixedId(options: PrefixedIdOptions): IdGenerator;
|
|
303
|
+
/**
|
|
304
|
+
* Custom ID plugin — injects generated IDs into documents before creation.
|
|
305
|
+
*
|
|
306
|
+
* @param options - Configuration for ID generation
|
|
307
|
+
* @returns Plugin instance
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* import { Repository, customIdPlugin, sequentialId } from '@classytic/mongokit';
|
|
312
|
+
*
|
|
313
|
+
* const invoiceRepo = new Repository(InvoiceModel, [
|
|
314
|
+
* customIdPlugin({
|
|
315
|
+
* field: 'invoiceNumber',
|
|
316
|
+
* generator: sequentialId({ prefix: 'INV', model: InvoiceModel }),
|
|
317
|
+
* }),
|
|
318
|
+
* ]);
|
|
319
|
+
*
|
|
320
|
+
* const inv = await invoiceRepo.create({ amount: 100 });
|
|
321
|
+
* console.log(inv.invoiceNumber); // "INV-0001"
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
declare function customIdPlugin(options: CustomIdOptions): Plugin;
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/plugins/multi-tenant.plugin.d.ts
|
|
327
|
+
/**
|
|
328
|
+
* Static tenant fields are picked from `@classytic/repo-core/tenant` so the
|
|
329
|
+
* vocabulary (`tenantField`, `contextKey`, `required`, `fieldType`) stays
|
|
330
|
+
* locked to the org-wide canonical source. Mongokit-specific runtime
|
|
331
|
+
* callbacks (`skipWhen`, `resolveContext`, `skipOperations`,
|
|
332
|
+
* `allowDataInjection`) extend on top — those genuinely differ from
|
|
333
|
+
* sqlitekit's runtime shape because mongokit's `RepositoryContext` carries
|
|
334
|
+
* Mongoose-specific bits the resolver may inspect.
|
|
335
|
+
*
|
|
336
|
+
* Mongokit defaults `fieldType` to `'string'` (not repo-core's `'objectId'`)
|
|
337
|
+
* for back-compat — pre-3.x stored tenant ids as raw strings; flipping the
|
|
338
|
+
* default would silently change cast behavior for existing apps.
|
|
339
|
+
*/
|
|
340
|
+
interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'contextKey' | 'required' | 'fieldType'> {
|
|
341
|
+
/** Operations to skip tenant injection (e.g., for admin/system queries) */
|
|
342
|
+
skipOperations?: string[];
|
|
343
|
+
/**
|
|
344
|
+
* Dynamic skip — receives the context and operation name, returns true to
|
|
345
|
+
* bypass tenant scoping for this call. Use for role-based bypass (e.g.,
|
|
346
|
+
* super admin) without needing a separate repo instance.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* skipWhen: (context) => context.role === 'superadmin'
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
skipWhen?: (context: RepositoryContext, operation: string) => boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Resolve tenant ID from external source (e.g., AsyncLocalStorage, CLS).
|
|
356
|
+
* Called when tenant ID is not found in context. If it returns a value,
|
|
357
|
+
* that value is used as the tenant ID without requiring it in context.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* resolveContext: () => asyncLocalStorage.getStore()?.tenantId
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
resolveContext?: () => string | undefined;
|
|
365
|
+
/**
|
|
366
|
+
* When `true`, bypass the `required` throw if the operation's policy
|
|
367
|
+
* target already carries the tenant field. This lets hosts that stamp
|
|
368
|
+
* the tenant into the payload themselves (e.g., `data[tenantField]`
|
|
369
|
+
* on writes, `filters[tenantField]` on paginated reads) use the plugin
|
|
370
|
+
* without having to hand-roll a `skipWhen` that duplicates this check.
|
|
371
|
+
*
|
|
372
|
+
* **Default: `false` (secure / fail-closed).** Tenant scope MUST come
|
|
373
|
+
* from `context` or `resolveContext`, otherwise the plugin throws
|
|
374
|
+
* (when `required: true`). This is the right posture for a security-
|
|
375
|
+
* sensitive boundary: caller-supplied scope on the payload cannot be
|
|
376
|
+
* trusted as authentication. If a host control-plane has already
|
|
377
|
+
* authenticated the tenant out-of-band and stamps it onto every
|
|
378
|
+
* write/read, set this to `true` explicitly — making the trust model
|
|
379
|
+
* visible at the call site.
|
|
380
|
+
*
|
|
381
|
+
* **Migration from < 4.0:** the prior default was `true`. To preserve
|
|
382
|
+
* the old behavior verbatim, set `allowDataInjection: true`. To get
|
|
383
|
+
* the new secure default, leave it unset.
|
|
384
|
+
*
|
|
385
|
+
* Composition: a user-supplied `skipWhen` still runs first and can
|
|
386
|
+
* always opt out. This option controls the behavior after `skipWhen`
|
|
387
|
+
* and `resolveContext` have had their turn.
|
|
388
|
+
*
|
|
389
|
+
* Safety: when the payload supplies the tenant AND this is `true`,
|
|
390
|
+
* the plugin skips its own injection (it does not overwrite). Cross-
|
|
391
|
+
* tenant isolation is therefore only as strong as the caller's own
|
|
392
|
+
* stamping — same guarantee as a hand-rolled `skipWhen`.
|
|
393
|
+
*/
|
|
394
|
+
allowDataInjection?: boolean;
|
|
395
|
+
/**
|
|
396
|
+
* What to do when the call's filter/payload ALREADY carries
|
|
397
|
+
* `tenantField` with a value that does NOT match the resolved tenant
|
|
398
|
+
* scope (a caller-supplied cross-tenant value — fat-fingered org id,
|
|
399
|
+
* stale client state, or an injection attempt).
|
|
400
|
+
*
|
|
401
|
+
* - `'throw'` (default, fail-closed): reject the call with a clear
|
|
402
|
+
* error. A mismatch is never legitimate on a scoped call — callers
|
|
403
|
+
* who genuinely need cross-tenant access use `bypassTenant: true`
|
|
404
|
+
* (per-call) or `skipWhen` (plugin-level), both of which emit the
|
|
405
|
+
* `after:tenant-bypass` audit event. Operator-shaped values
|
|
406
|
+
* (`{ $in: [...] }`) on the tenant field can't be verified against
|
|
407
|
+
* the scope and are rejected too — multi-value tenant scoping is
|
|
408
|
+
* deliberately not supported (see CLAUDE.md); use an explicit
|
|
409
|
+
* bypass + manual filter.
|
|
410
|
+
* - `'overwrite'` (legacy, pre-3.16 behavior): silently replace the
|
|
411
|
+
* supplied value with the resolved scope. Still leak-safe (the
|
|
412
|
+
* resolved scope always wins) but hides caller bugs.
|
|
413
|
+
*
|
|
414
|
+
* Matching values (same tenant, any representation — string vs
|
|
415
|
+
* ObjectId) are never a mismatch; the plugin normalizes them to the
|
|
416
|
+
* configured `fieldType`.
|
|
417
|
+
*/
|
|
418
|
+
onMismatch?: 'throw' | 'overwrite';
|
|
419
|
+
}
|
|
420
|
+
declare function multiTenantPlugin(options?: MultiTenantOptions): Plugin;
|
|
421
|
+
/**
|
|
422
|
+
* Build the canonical `skipWhen` callback for role-based tenant
|
|
423
|
+
* bypass — replaces the hand-rolled
|
|
424
|
+
* `(ctx) => ctx.role === 'superadmin'` callback that ~6 packages
|
|
425
|
+
* across the classytic codebase carry as boilerplate. Eliminates the
|
|
426
|
+
* "I forgot to include 'platform_admin'" drift class — the role
|
|
427
|
+
* vocabulary lives in one place.
|
|
428
|
+
*
|
|
429
|
+
* **Composes with `bypassTenant: true` per-call**, doesn't replace
|
|
430
|
+
* it. `bypassTenant` is the deliberate per-call form for migration
|
|
431
|
+
* scripts and one-off admin queries; `adminBypass` is the
|
|
432
|
+
* always-on role-based bypass for users whose JWT / session carries
|
|
433
|
+
* a privileged role.
|
|
434
|
+
*
|
|
435
|
+
* **NOT a substitute for authentication or RBAC.** This factory only
|
|
436
|
+
* answers "should the tenant scope hook fire for this call?" — it
|
|
437
|
+
* does NOT validate that the caller is authenticated, that their
|
|
438
|
+
* role claim is genuine, or that the requested operation is
|
|
439
|
+
* permitted. Always run auth + RBAC before the call reaches the
|
|
440
|
+
* repository layer; this is the LAST gate, not the only one.
|
|
441
|
+
*
|
|
442
|
+
* @example Single role
|
|
443
|
+
* ```ts
|
|
444
|
+
* import { adminBypass, multiTenantPlugin } from '@classytic/mongokit';
|
|
445
|
+
*
|
|
446
|
+
* const repo = new Repository(InvoiceModel, [
|
|
447
|
+
* multiTenantPlugin({
|
|
448
|
+
* tenantField: 'organizationId',
|
|
449
|
+
* skipWhen: adminBypass({ adminRoles: ['superadmin'] }),
|
|
450
|
+
* }),
|
|
451
|
+
* ]);
|
|
452
|
+
*
|
|
453
|
+
* await repo.getAll({}, { role: 'superadmin' }); // sees all orgs
|
|
454
|
+
* await repo.getAll({}, { role: 'user', organizationId: 'org-1' }); // scoped
|
|
455
|
+
* ```
|
|
456
|
+
*
|
|
457
|
+
* @example Multiple admin roles + custom field name
|
|
458
|
+
* ```ts
|
|
459
|
+
* skipWhen: adminBypass({
|
|
460
|
+
* roleField: 'principalRole', // ctx.principalRole, not ctx.role
|
|
461
|
+
* adminRoles: ['superadmin', 'platform_admin', 'support'],
|
|
462
|
+
* }),
|
|
463
|
+
* ```
|
|
464
|
+
*
|
|
465
|
+
* @example Composing with custom logic
|
|
466
|
+
* ```ts
|
|
467
|
+
* // adminBypass for the role check, then your own additional logic:
|
|
468
|
+
* const adminCheck = adminBypass({ adminRoles: ['superadmin'] });
|
|
469
|
+
* skipWhen: (ctx, op) => adminCheck(ctx, op) || ctx.internalSystemFlag === true,
|
|
470
|
+
* ```
|
|
471
|
+
*
|
|
472
|
+
* @param options.roleField - Path on the context to read the role
|
|
473
|
+
* from. Defaults to `'role'`. Top-level keys only — for nested
|
|
474
|
+
* shapes (e.g. `ctx.user.role`), wrap your own `(ctx) => …` callback
|
|
475
|
+
* instead.
|
|
476
|
+
* @param options.adminRoles - Role values that grant the bypass. The
|
|
477
|
+
* factory does an exact-match `includes` check — case-sensitive,
|
|
478
|
+
* no fuzzy matching. Lowercase your role vocabulary upstream.
|
|
479
|
+
* @returns A `skipWhen`-compatible callback `(ctx, op) → boolean`.
|
|
480
|
+
*/
|
|
481
|
+
declare function adminBypass(options: {
|
|
482
|
+
roleField?: string;
|
|
483
|
+
adminRoles: readonly string[];
|
|
484
|
+
}): (context: RepositoryContext, operation: string) => boolean;
|
|
485
|
+
//#endregion
|
|
486
|
+
export { AuditTrailMethods as C, auditTrailPlugin as E, AuditQueryResult as S, AuditTrailQuery as T, RepositoryCachePluginOptions as _, DateSequentialIdOptions as a, AuditOperation as b, SequentialIdOptions as c, getNextSequence as d, prefixedId as f, RepositoryCacheHandle as g, CacheOptions as h, CustomIdOptions as i, customIdPlugin as l, CacheAdapter$1 as m, adminBypass as n, IdGenerator as o, sequentialId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, cachePlugin as v, AuditTrailOptions as w, AuditQueryOptions as x, AuditEntry as y };
|