@classytic/mongokit 3.12.0 → 3.13.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 +74 -4
- package/dist/{PaginationEngine-BDaAsyPn.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
- package/dist/Repository-BY40uhcv.mjs +3603 -0
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +190 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter-DTDMnUOH.mjs +158 -0
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +1 -1
- package/dist/better-auth/index.d.mts +149 -0
- package/dist/better-auth/index.mjs +81 -0
- package/dist/field-selection-Br3WPOEb.mjs +99 -0
- package/dist/{index-D9cORR-o.d.mts → index-BbHaKwi5.d.mts} +33 -13
- package/dist/index.d.mts +667 -30
- package/dist/index.mjs +240 -17
- package/dist/{logger-8-VS1za6.mjs → logger-DprijP5h.mjs} +1 -5
- package/dist/{mongooseToJsonSchema-B187ufOP.mjs → mongooseToJsonSchema-BYbG894y.mjs} +1 -1
- package/dist/{mongooseToJsonSchema-rtkdbBhf.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +2 -1
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{types-DXscZnLu.d.mts → types-BbaPZleN.d.mts} +371 -112
- package/dist/{update-CDxPtpsk.mjs → update-DLcSSa5b.mjs} +41 -27
- package/dist/utils/index.d.mts +2 -91
- package/dist/utils/index.mjs +5 -5
- package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +212 -80
- package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
- package/package.json +20 -3
- package/dist/field-selection-dI0m4RBs.mjs +0 -221
- package/dist/validation-chain.plugin-BicIgzzt.mjs +0 -4970
- /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
- /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
- /package/dist/{indexes-C26GNZRU.d.mts → indexes-CgOF7Wjd.d.mts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { n as warn } from "./logger-DprijP5h.mjs";
|
|
3
3
|
import { t as createError } from "./error-B00pI7dV.mjs";
|
|
4
4
|
import { isFilter } from "@classytic/repo-core/filter";
|
|
5
5
|
//#region src/filter/compile.ts
|
|
@@ -475,17 +475,17 @@ async function aggregatePaginate(Model, pipeline, options = {}) {
|
|
|
475
475
|
const skip = (page - 1) * limit;
|
|
476
476
|
if (limit > 1e3) warn(`[mongokit] Large aggregation limit (${limit}). $facet results must be <16MB. Consider using Repository.aggregatePaginate() for safer handling of large datasets.`);
|
|
477
477
|
const facetPipeline = [...pipeline, { $facet: {
|
|
478
|
-
|
|
478
|
+
data: [{ $skip: skip }, { $limit: limit }],
|
|
479
479
|
total: [{ $count: "count" }]
|
|
480
480
|
} }];
|
|
481
481
|
const aggregation = Model.aggregate(facetPipeline);
|
|
482
482
|
if (options.session) aggregation.session(options.session);
|
|
483
483
|
const [result] = await aggregation.exec();
|
|
484
|
-
const
|
|
484
|
+
const data = result.data || [];
|
|
485
485
|
const total = result.total[0]?.count || 0;
|
|
486
486
|
const pages = Math.ceil(total / limit);
|
|
487
487
|
return {
|
|
488
|
-
|
|
488
|
+
data,
|
|
489
489
|
total,
|
|
490
490
|
page,
|
|
491
491
|
limit,
|
|
@@ -662,54 +662,49 @@ var delete_exports = /* @__PURE__ */ __exportAll({
|
|
|
662
662
|
softDelete: () => softDelete
|
|
663
663
|
});
|
|
664
664
|
/**
|
|
665
|
-
* Delete by ID
|
|
665
|
+
* Delete by ID. Returns `null` on miss (matches `update()` convention) —
|
|
666
|
+
* a second delete on the same id is a no-op, not an error.
|
|
666
667
|
*/
|
|
667
668
|
async function deleteById(Model, id, options = {}) {
|
|
668
669
|
const query = {
|
|
669
670
|
_id: id,
|
|
670
671
|
...options.query
|
|
671
672
|
};
|
|
672
|
-
if (!await Model.findOneAndDelete(query).session(options.session ?? null)) return
|
|
673
|
-
success: false,
|
|
674
|
-
message: "Document not found",
|
|
675
|
-
id: String(id)
|
|
676
|
-
};
|
|
673
|
+
if (!await Model.findOneAndDelete(query).session(options.session ?? null)) return null;
|
|
677
674
|
return {
|
|
678
|
-
success: true,
|
|
679
675
|
message: "Deleted successfully",
|
|
680
676
|
id: String(id)
|
|
681
677
|
};
|
|
682
678
|
}
|
|
683
679
|
/**
|
|
684
|
-
* Delete many documents
|
|
680
|
+
* Delete many documents. Always returns a {@link DeleteResult} (never
|
|
681
|
+
* null) — a deleteMany with zero matches is a successful op with
|
|
682
|
+
* `count: 0`, not a miss.
|
|
685
683
|
*/
|
|
686
684
|
async function deleteMany(Model, query, options = {}) {
|
|
687
685
|
return {
|
|
688
|
-
success: true,
|
|
689
686
|
count: (await Model.deleteMany(query).session(options.session ?? null)).deletedCount,
|
|
690
687
|
message: "Deleted successfully"
|
|
691
688
|
};
|
|
692
689
|
}
|
|
693
690
|
/**
|
|
694
|
-
* Delete by query
|
|
691
|
+
* Delete by query. Returns `null` on miss unless `throwOnNotFound: true`.
|
|
695
692
|
*/
|
|
696
693
|
async function deleteByQuery(Model, query, options = {}) {
|
|
697
694
|
const document = await Model.findOneAndDelete(query).session(options.session ?? null);
|
|
698
695
|
if (!document) {
|
|
699
696
|
if (options.throwOnNotFound === true) throw createError(404, "Document not found");
|
|
700
|
-
return
|
|
701
|
-
success: false,
|
|
702
|
-
message: "Document not found"
|
|
703
|
-
};
|
|
697
|
+
return null;
|
|
704
698
|
}
|
|
705
699
|
return {
|
|
706
|
-
success: true,
|
|
707
700
|
message: "Deleted successfully",
|
|
708
701
|
id: String(document._id)
|
|
709
702
|
};
|
|
710
703
|
}
|
|
711
704
|
/**
|
|
712
|
-
* Soft delete (set deleted flag)
|
|
705
|
+
* Soft delete (set deleted flag). Returns `null` on miss for parity with
|
|
706
|
+
* `deleteById`; callers that prefer a throw can wrap or use `throwOnNotFound`
|
|
707
|
+
* pattern at the call site.
|
|
713
708
|
*/
|
|
714
709
|
async function softDelete(Model, id, options = {}) {
|
|
715
710
|
if (!await Model.findByIdAndUpdate(id, {
|
|
@@ -719,16 +714,15 @@ async function softDelete(Model, id, options = {}) {
|
|
|
719
714
|
}, {
|
|
720
715
|
returnDocument: "after",
|
|
721
716
|
session: options.session
|
|
722
|
-
}))
|
|
717
|
+
})) return null;
|
|
723
718
|
return {
|
|
724
|
-
success: true,
|
|
725
719
|
message: "Soft deleted successfully",
|
|
726
720
|
id: String(id),
|
|
727
721
|
soft: true
|
|
728
722
|
};
|
|
729
723
|
}
|
|
730
724
|
/**
|
|
731
|
-
* Restore soft
|
|
725
|
+
* Restore soft-deleted document. Returns `null` on miss.
|
|
732
726
|
*/
|
|
733
727
|
async function restore(Model, id, options = {}) {
|
|
734
728
|
if (!await Model.findByIdAndUpdate(id, {
|
|
@@ -738,9 +732,8 @@ async function restore(Model, id, options = {}) {
|
|
|
738
732
|
}, {
|
|
739
733
|
returnDocument: "after",
|
|
740
734
|
session: options.session
|
|
741
|
-
}))
|
|
735
|
+
})) return null;
|
|
742
736
|
return {
|
|
743
|
-
success: true,
|
|
744
737
|
message: "Restored successfully",
|
|
745
738
|
id: String(id)
|
|
746
739
|
};
|
|
@@ -834,16 +827,37 @@ async function getAll(Model, query = {}, options = {}) {
|
|
|
834
827
|
return mongoQuery.exec();
|
|
835
828
|
}
|
|
836
829
|
/**
|
|
837
|
-
* Get or create document (upsert)
|
|
830
|
+
* Get or create document (atomic upsert).
|
|
831
|
+
*
|
|
832
|
+
* Returns `{ doc, created }` so callers can disambiguate the two
|
|
833
|
+
* outcomes without a follow-up read — load-bearing for race-detection
|
|
834
|
+
* in idempotency stores, lock acquisition, and "ensure-exists" flows.
|
|
835
|
+
*
|
|
836
|
+
* `created: true` ⇔ this caller's `createData` won the race and was
|
|
837
|
+
* just inserted; `created: false` ⇔ a doc matching `query` already
|
|
838
|
+
* existed and was returned unchanged.
|
|
839
|
+
*
|
|
840
|
+
* Mongo: `includeResultMetadata: true` returns the raw `ModifyResult`
|
|
841
|
+
* (`{ value, lastErrorObject, ok }`). `lastErrorObject.upserted` is
|
|
842
|
+
* the `_id` of a freshly-inserted row — present iff this call inserted.
|
|
838
843
|
*/
|
|
839
844
|
async function getOrCreate(Model, query, createData, options = {}) {
|
|
840
|
-
|
|
845
|
+
const result = await Model.findOneAndUpdate(query, { $setOnInsert: createData }, {
|
|
841
846
|
upsert: true,
|
|
842
847
|
returnDocument: "after",
|
|
843
848
|
runValidators: true,
|
|
849
|
+
includeResultMetadata: true,
|
|
844
850
|
session: options.session,
|
|
845
851
|
...options.updatePipeline !== void 0 ? { updatePipeline: options.updatePipeline } : {}
|
|
846
852
|
});
|
|
853
|
+
if (result && typeof result === "object" && "value" in result) return {
|
|
854
|
+
doc: result.value,
|
|
855
|
+
created: result.lastErrorObject?.upserted != null
|
|
856
|
+
};
|
|
857
|
+
return {
|
|
858
|
+
doc: result,
|
|
859
|
+
created: false
|
|
860
|
+
};
|
|
847
861
|
}
|
|
848
862
|
/**
|
|
849
863
|
* Count documents matching query
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,94 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-rtkdbBhf.mjs";
|
|
1
|
+
import { a as createFieldPreset, c as getMongooseProjection, i as configureLogger, l as createError, n as buildCrudSchemasFromMongooseSchema, o as filterResponseData, r as createMemoryCache, s as getFieldsForUser, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-BgRAaXX-.mjs";
|
|
3
2
|
|
|
4
|
-
//#region src/utils/cache-keys.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* Generate cache key for getById operations
|
|
7
|
-
*
|
|
8
|
-
* Includes select/populate/lean in the key so different query shapes
|
|
9
|
-
* (e.g., plain vs populated, lean vs hydrated) never collide.
|
|
10
|
-
*
|
|
11
|
-
* Format: {prefix}:id:{model}:{documentId} (no options)
|
|
12
|
-
* Format: {prefix}:id:{model}:{documentId}:{optionsHash} (with options)
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* byIdKey('mk', 'User', '507f1f77bcf86cd799439011')
|
|
16
|
-
* // => 'mk:id:User:507f1f77bcf86cd799439011'
|
|
17
|
-
* byIdKey('mk', 'User', '507f1f77bcf86cd799439011', { select: 'name email' })
|
|
18
|
-
* // => 'mk:id:User:507f1f77bcf86cd799439011:a1b2c3d4'
|
|
19
|
-
*/
|
|
20
|
-
declare function byIdKey(prefix: string, model: string, id: string, options?: {
|
|
21
|
-
select?: SelectSpec;
|
|
22
|
-
populate?: PopulateSpec;
|
|
23
|
-
lean?: boolean;
|
|
24
|
-
}): string;
|
|
25
|
-
/**
|
|
26
|
-
* Generate cache key for single-document queries
|
|
27
|
-
*
|
|
28
|
-
* Format: {prefix}:one:{model}:{queryHash}
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* byQueryKey('mk', 'User', { email: 'john@example.com' })
|
|
32
|
-
* // => 'mk:one:User:a1b2c3d4'
|
|
33
|
-
*/
|
|
34
|
-
declare function byQueryKey(prefix: string, model: string, version: number, query: Record<string, unknown>, options?: {
|
|
35
|
-
select?: SelectSpec;
|
|
36
|
-
populate?: PopulateSpec;
|
|
37
|
-
}): string;
|
|
38
|
-
/**
|
|
39
|
-
* Generate cache key for paginated list queries
|
|
40
|
-
*
|
|
41
|
-
* Format: {prefix}:list:{model}:{version}:{queryHash}
|
|
42
|
-
*
|
|
43
|
-
* The version component enables efficient bulk invalidation:
|
|
44
|
-
* - On any mutation, bump the version
|
|
45
|
-
* - All list cache keys become invalid without scanning/deleting each
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* listQueryKey('mk', 'User', 1, { filters: { status: 'active' }, page: 1, limit: 20 })
|
|
49
|
-
* // => 'mk:list:User:1:e5f6g7h8'
|
|
50
|
-
*/
|
|
51
|
-
declare function listQueryKey(prefix: string, model: string, version: number, params: {
|
|
52
|
-
filters?: Record<string, unknown>;
|
|
53
|
-
sort?: SortSpec;
|
|
54
|
-
page?: number;
|
|
55
|
-
limit?: number;
|
|
56
|
-
after?: string;
|
|
57
|
-
select?: SelectSpec;
|
|
58
|
-
populate?: PopulateSpec;
|
|
59
|
-
search?: string;
|
|
60
|
-
mode?: string;
|
|
61
|
-
lean?: boolean;
|
|
62
|
-
readPreference?: string;
|
|
63
|
-
hint?: string | Record<string, unknown>;
|
|
64
|
-
maxTimeMS?: number;
|
|
65
|
-
countStrategy?: string;
|
|
66
|
-
}): string;
|
|
67
|
-
/**
|
|
68
|
-
* Generate cache key for collection version tag
|
|
69
|
-
*
|
|
70
|
-
* Format: {prefix}:ver:{model}
|
|
71
|
-
*
|
|
72
|
-
* Used to track mutation version for list invalidation
|
|
73
|
-
*/
|
|
74
|
-
declare function versionKey(prefix: string, model: string): string;
|
|
75
|
-
/**
|
|
76
|
-
* Generate pattern for clearing all cache keys for a model
|
|
77
|
-
*
|
|
78
|
-
* Format: {prefix}:*:{model}:*
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* modelPattern('mk', 'User')
|
|
82
|
-
* // => 'mk:*:User:*'
|
|
83
|
-
*/
|
|
84
|
-
declare function modelPattern(prefix: string, model: string): string;
|
|
85
|
-
/**
|
|
86
|
-
* Generate pattern for clearing all list cache keys for a model
|
|
87
|
-
*
|
|
88
|
-
* Format: {prefix}:list:{model}:*
|
|
89
|
-
*/
|
|
90
|
-
declare function listPattern(prefix: string, model: string): string;
|
|
91
|
-
//#endregion
|
|
92
3
|
//#region src/utils/id-resolution.d.ts
|
|
93
4
|
/**
|
|
94
5
|
* ID Resolution Primitive
|
|
@@ -146,4 +57,4 @@ declare function getSchemaIdType(schema?: SchemaLike | null): IdType;
|
|
|
146
57
|
*/
|
|
147
58
|
declare function isValidIdForType(id: unknown, idType: IdType): boolean;
|
|
148
59
|
//#endregion
|
|
149
|
-
export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema,
|
|
60
|
+
export { type IdType, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as configureLogger } from "../logger-
|
|
1
|
+
import { t as configureLogger } from "../logger-DprijP5h.mjs";
|
|
2
2
|
import { t as createError } from "../error-B00pI7dV.mjs";
|
|
3
|
-
import { n as
|
|
4
|
-
import {
|
|
5
|
-
import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-
|
|
6
|
-
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema,
|
|
3
|
+
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "../field-selection-Br3WPOEb.mjs";
|
|
4
|
+
import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-B0Odvvou.mjs";
|
|
5
|
+
import { n as buildCrudSchemasFromMongooseSchema, r as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-BYbG894y.mjs";
|
|
6
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, isObjectIdInstance, isValidIdForType };
|
package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts}
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as ObjectId, C as FieldPreset, G as Plugin, J as PopulateSpec, Q as RepositoryContext, _t as ValidationChainOptions, et as RepositoryInstance, it as SelectSpec, j as Logger, l as CascadeOptions, st as SoftDeleteOptions, ut as SortSpec, vt as ValidatorDefinition } from "./types-BbaPZleN.mjs";
|
|
2
2
|
import mongoose from "mongoose";
|
|
3
|
+
import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
|
|
3
4
|
import { BulkWriteResult } from "@classytic/repo-core/repository";
|
|
4
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
@@ -91,12 +92,6 @@ interface AggregateHelpersMethods {
|
|
|
91
92
|
}
|
|
92
93
|
//#endregion
|
|
93
94
|
//#region src/plugins/audit-log.plugin.d.ts
|
|
94
|
-
/**
|
|
95
|
-
* Audit log plugin that logs all repository operations
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* const repo = new Repository(Model, [auditLogPlugin(console)]);
|
|
99
|
-
*/
|
|
100
95
|
declare function auditLogPlugin(logger: Logger): Plugin;
|
|
101
96
|
//#endregion
|
|
102
97
|
//#region src/plugins/audit-trail.plugin.d.ts
|
|
@@ -121,6 +116,22 @@ interface AuditTrailOptions {
|
|
|
121
116
|
* These fields are redacted in the `changes` diff.
|
|
122
117
|
*/
|
|
123
118
|
excludeFields?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Custom callback fired when an audit write fails. Receives the error
|
|
121
|
+
* and the entry that failed to land. Default: `console.warn` only.
|
|
122
|
+
*
|
|
123
|
+
* **Use this for compliance-grade audit trails (SOC 2 / PCI / HIPAA)
|
|
124
|
+
* where missing entries must be surfaced upstream** — e.g. forward to
|
|
125
|
+
* a dead-letter queue, page on-call, or short-circuit the request.
|
|
126
|
+
* The default `warn` log is appropriate for development and
|
|
127
|
+
* eventually-consistent observability use cases but is invisible to
|
|
128
|
+
* compliance review.
|
|
129
|
+
*
|
|
130
|
+
* The callback runs on the same fire-and-forget microtask that wrote
|
|
131
|
+
* the entry, so throwing from here will surface as an unhandled
|
|
132
|
+
* rejection — handle (or rethrow into your error reporter) inside.
|
|
133
|
+
*/
|
|
134
|
+
onWriteError?: (error: Error, entry: Omit<AuditEntry, 'timestamp'>) => void;
|
|
124
135
|
}
|
|
125
136
|
type AuditOperation = 'create' | 'update' | 'delete' | 'findOneAndUpdate';
|
|
126
137
|
interface AuditEntry {
|
|
@@ -150,7 +161,7 @@ interface AuditQueryOptions {
|
|
|
150
161
|
limit?: number;
|
|
151
162
|
}
|
|
152
163
|
interface AuditQueryResult {
|
|
153
|
-
|
|
164
|
+
data: AuditEntry[];
|
|
154
165
|
page: number;
|
|
155
166
|
limit: number;
|
|
156
167
|
total: number;
|
|
@@ -286,64 +297,6 @@ interface BatchOperationsMethods {
|
|
|
286
297
|
}): Promise<BulkWriteResult>;
|
|
287
298
|
}
|
|
288
299
|
//#endregion
|
|
289
|
-
//#region src/plugins/cache.plugin.d.ts
|
|
290
|
-
/**
|
|
291
|
-
* Cache plugin factory
|
|
292
|
-
*
|
|
293
|
-
* @param options - Cache configuration
|
|
294
|
-
* @returns Plugin instance
|
|
295
|
-
*/
|
|
296
|
-
declare function cachePlugin(options: CacheOptions): Plugin;
|
|
297
|
-
/**
|
|
298
|
-
* TypeScript interface for cache plugin methods
|
|
299
|
-
*
|
|
300
|
-
* @example
|
|
301
|
-
* ```typescript
|
|
302
|
-
* import type { CacheMethods } from '@classytic/mongokit';
|
|
303
|
-
*
|
|
304
|
-
* type ProductRepoWithCache = ProductRepo & CacheMethods;
|
|
305
|
-
*
|
|
306
|
-
* const productRepo = new ProductRepo(ProductModel, [
|
|
307
|
-
* methodRegistryPlugin(),
|
|
308
|
-
* cachePlugin({ adapter: redisAdapter, ttl: 60 }),
|
|
309
|
-
* ]) as ProductRepoWithCache;
|
|
310
|
-
*
|
|
311
|
-
* // TypeScript autocomplete for cache methods
|
|
312
|
-
* await productRepo.invalidateCache(productId);
|
|
313
|
-
* await productRepo.invalidateListCache();
|
|
314
|
-
* await productRepo.invalidateAllCache();
|
|
315
|
-
* const stats = productRepo.getCacheStats();
|
|
316
|
-
* productRepo.resetCacheStats();
|
|
317
|
-
* ```
|
|
318
|
-
*/
|
|
319
|
-
interface CacheMethods {
|
|
320
|
-
/**
|
|
321
|
-
* Invalidate cache for a specific document
|
|
322
|
-
* Use when document was updated outside this service
|
|
323
|
-
* @param id - Document ID to invalidate
|
|
324
|
-
*/
|
|
325
|
-
invalidateCache(id: string): Promise<void>;
|
|
326
|
-
/**
|
|
327
|
-
* Invalidate all list caches for this model
|
|
328
|
-
* Use when bulk changes happened outside this service
|
|
329
|
-
*/
|
|
330
|
-
invalidateListCache(): Promise<void>;
|
|
331
|
-
/**
|
|
332
|
-
* Invalidate ALL cache entries for this model
|
|
333
|
-
* Nuclear option - use sparingly
|
|
334
|
-
*/
|
|
335
|
-
invalidateAllCache(): Promise<void>;
|
|
336
|
-
/**
|
|
337
|
-
* Get cache statistics for monitoring
|
|
338
|
-
* @returns Cache statistics (hits, misses, sets, invalidations)
|
|
339
|
-
*/
|
|
340
|
-
getCacheStats(): CacheStats;
|
|
341
|
-
/**
|
|
342
|
-
* Reset cache statistics
|
|
343
|
-
*/
|
|
344
|
-
resetCacheStats(): void;
|
|
345
|
-
}
|
|
346
|
-
//#endregion
|
|
347
300
|
//#region src/plugins/cascade.plugin.d.ts
|
|
348
301
|
/**
|
|
349
302
|
* Cascade delete plugin.
|
|
@@ -535,6 +488,85 @@ declare function elasticSearchPlugin(options: ElasticSearchOptions): Plugin;
|
|
|
535
488
|
*/
|
|
536
489
|
declare function fieldFilterPlugin(fieldPreset: FieldPreset): Plugin;
|
|
537
490
|
//#endregion
|
|
491
|
+
//#region src/plugins/lease.plugin.d.ts
|
|
492
|
+
interface LeasePluginOptions {
|
|
493
|
+
/** Field carrying the row's status. @default `'status'` */
|
|
494
|
+
statusField?: string;
|
|
495
|
+
/** Field stamping the lease holder. @default `'leasedBy'` */
|
|
496
|
+
leasedByField?: string;
|
|
497
|
+
/** Field carrying the lease expiry timestamp. @default `'leaseExpiresAt'` */
|
|
498
|
+
leaseExpiresAtField?: string;
|
|
499
|
+
/** Status value an unleased row carries. @default `'pending'` */
|
|
500
|
+
pendingStatus?: string;
|
|
501
|
+
/** Status value a leased row carries. @default `'processing'` */
|
|
502
|
+
processingStatus?: string;
|
|
503
|
+
/** Status value a successfully released row carries. @default `'done'` */
|
|
504
|
+
doneStatus?: string;
|
|
505
|
+
/** Sort fields used to pick the FIFO winner from the lease pool. @default `{ createdAt: 1 }` */
|
|
506
|
+
sort?: Record<string, 1 | -1>;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Methods contributed by `leasePlugin()`. Use as a type assertion when
|
|
510
|
+
* constructing the repo so call sites get autocomplete:
|
|
511
|
+
*
|
|
512
|
+
* ```ts
|
|
513
|
+
* type OutboxRepo = Repository<IOutboxRow> & LeaseMethods<IOutboxRow>;
|
|
514
|
+
* const repo = new Repository(OutboxModel, [
|
|
515
|
+
* methodRegistryPlugin(),
|
|
516
|
+
* leasePlugin({ pendingStatus: 'queued' }),
|
|
517
|
+
* ]) as OutboxRepo;
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
interface LeaseMethods<TDoc> {
|
|
521
|
+
/**
|
|
522
|
+
* Atomically claim the next available row. Matches rows that are
|
|
523
|
+
* either in `pendingStatus` OR have an expired `leaseExpiresAt` (dead
|
|
524
|
+
* lease recovery). Sets status to `processingStatus`, stamps
|
|
525
|
+
* `leasedBy`, and pushes `leaseExpiresAt = now + leaseFor`.
|
|
526
|
+
*
|
|
527
|
+
* @returns The leased doc, or `null` when no row matches.
|
|
528
|
+
*/
|
|
529
|
+
lease(opts: {
|
|
530
|
+
filter?: Record<string, unknown>;
|
|
531
|
+
leaseFor: number;
|
|
532
|
+
leasedBy: string;
|
|
533
|
+
options?: Record<string, unknown>;
|
|
534
|
+
}): Promise<TDoc | null>;
|
|
535
|
+
/**
|
|
536
|
+
* Push a held lease's expiry further. Only succeeds when the lease
|
|
537
|
+
* is still ours (`leasedBy === leasedBy && leaseExpiresAt > now`).
|
|
538
|
+
* Returns `null` if the lease was lost — caller should drop the
|
|
539
|
+
* work, since someone else has already started replaying it.
|
|
540
|
+
*/
|
|
541
|
+
extend(id: string | ObjectId, opts: {
|
|
542
|
+
leasedBy: string;
|
|
543
|
+
leaseFor: number;
|
|
544
|
+
options?: Record<string, unknown>;
|
|
545
|
+
}): Promise<TDoc | null>;
|
|
546
|
+
/**
|
|
547
|
+
* Release a held lease. CAS-checks `leasedBy === opts.leasedBy` AND
|
|
548
|
+
* `leaseExpiresAt > now` so only the live lease holder may finalise
|
|
549
|
+
* the row — symmetrical with `extend()`. A worker whose lease has
|
|
550
|
+
* been recovered (lease lost) MUST NOT mark someone else's
|
|
551
|
+
* in-progress work as `done` / `failed`; this method returns `null`
|
|
552
|
+
* in that race rather than overwriting.
|
|
553
|
+
*
|
|
554
|
+
* Sets status to `opts.finalStatus` (default `doneStatus`), clears
|
|
555
|
+
* `leasedBy` and `leaseExpiresAt`. Use `{ finalStatus: 'failed' }`
|
|
556
|
+
* for error paths so terminal-status filters (next caller's
|
|
557
|
+
* `lease()` predicate) skip this row.
|
|
558
|
+
*
|
|
559
|
+
* Returns the released doc, or `null` when the CAS lost (lease
|
|
560
|
+
* expired / held by another worker / row gone).
|
|
561
|
+
*/
|
|
562
|
+
release(id: string | ObjectId, opts: {
|
|
563
|
+
leasedBy: string;
|
|
564
|
+
finalStatus?: string;
|
|
565
|
+
options?: Record<string, unknown>;
|
|
566
|
+
}): Promise<TDoc | null>;
|
|
567
|
+
}
|
|
568
|
+
declare function leasePlugin(options?: LeasePluginOptions): Plugin;
|
|
569
|
+
//#endregion
|
|
538
570
|
//#region src/plugins/method-registry.plugin.d.ts
|
|
539
571
|
/**
|
|
540
572
|
* Extended repository interface with method registry
|
|
@@ -752,6 +784,33 @@ interface MongoOperationsMethods<TDoc> {
|
|
|
752
784
|
* });
|
|
753
785
|
*/
|
|
754
786
|
atomicUpdate(id: string | ObjectId, operators: Record<string, Record<string, unknown>>, options?: Record<string, unknown>): Promise<TDoc>;
|
|
787
|
+
/**
|
|
788
|
+
* Atomic conditional increment — increments the field only when it is
|
|
789
|
+
* strictly below `ceiling`. Returns the updated document on success,
|
|
790
|
+
* `null` when already at/above the ceiling.
|
|
791
|
+
*
|
|
792
|
+
* Single-round-trip CAS via `findOneAndUpdate({ _id, field: { $lt:
|
|
793
|
+
* ceiling }}, { $inc: { field: by }})`. Use for per-document
|
|
794
|
+
* concurrency limits, quotas, and capacity caps where a read-then-
|
|
795
|
+
* write loop would race.
|
|
796
|
+
*
|
|
797
|
+
* @param id - Document ID
|
|
798
|
+
* @param field - Numeric field to increment
|
|
799
|
+
* @param ceiling - Increment only when the field is `< ceiling`
|
|
800
|
+
* @param by - Increment delta (default `1`; pass negative to decrement-with-floor by inverting the predicate via the call site)
|
|
801
|
+
* @param options - Operation options (session, etc.)
|
|
802
|
+
* @returns The updated document, or `null` when at/above ceiling
|
|
803
|
+
*
|
|
804
|
+
* @example Per-tenant concurrency limit
|
|
805
|
+
* ```ts
|
|
806
|
+
* const claimed = await repo.incrementIfBelow(tenantId, 'activeRuns', 10);
|
|
807
|
+
* if (!claimed) throw new ConcurrencyLimitExceeded();
|
|
808
|
+
* try { await runWorkflow(); } finally {
|
|
809
|
+
* await repo.decrement(tenantId, 'activeRuns');
|
|
810
|
+
* }
|
|
811
|
+
* ```
|
|
812
|
+
*/
|
|
813
|
+
incrementIfBelow(id: string | ObjectId, field: string, ceiling: number, by?: number, options?: Record<string, unknown>): Promise<TDoc | null>;
|
|
755
814
|
}
|
|
756
815
|
//#endregion
|
|
757
816
|
//#region src/plugins/multi-tenant.plugin.d.ts
|
|
@@ -794,28 +853,101 @@ interface MultiTenantOptions extends Pick<TenantConfig, 'tenantField' | 'context
|
|
|
794
853
|
*/
|
|
795
854
|
resolveContext?: () => string | undefined;
|
|
796
855
|
/**
|
|
797
|
-
* When `true
|
|
798
|
-
*
|
|
799
|
-
*
|
|
856
|
+
* When `true`, bypass the `required` throw if the operation's policy
|
|
857
|
+
* target already carries the tenant field. This lets hosts that stamp
|
|
858
|
+
* the tenant into the payload themselves (e.g., `data[tenantField]`
|
|
800
859
|
* on writes, `filters[tenantField]` on paginated reads) use the plugin
|
|
801
860
|
* without having to hand-roll a `skipWhen` that duplicates this check.
|
|
802
861
|
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
862
|
+
* **Default: `false` (secure / fail-closed).** Tenant scope MUST come
|
|
863
|
+
* from `context` or `resolveContext`, otherwise the plugin throws
|
|
864
|
+
* (when `required: true`). This is the right posture for a security-
|
|
865
|
+
* sensitive boundary: caller-supplied scope on the payload cannot be
|
|
866
|
+
* trusted as authentication. If a host control-plane has already
|
|
867
|
+
* authenticated the tenant out-of-band and stamps it onto every
|
|
868
|
+
* write/read, set this to `true` explicitly — making the trust model
|
|
869
|
+
* visible at the call site.
|
|
806
870
|
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
871
|
+
* **Migration from < 4.0:** the prior default was `true`. To preserve
|
|
872
|
+
* the old behavior verbatim, set `allowDataInjection: true`. To get
|
|
873
|
+
* the new secure default, leave it unset.
|
|
810
874
|
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
875
|
+
* Composition: a user-supplied `skipWhen` still runs first and can
|
|
876
|
+
* always opt out. This option controls the behavior after `skipWhen`
|
|
877
|
+
* and `resolveContext` have had their turn.
|
|
878
|
+
*
|
|
879
|
+
* Safety: when the payload supplies the tenant AND this is `true`,
|
|
880
|
+
* the plugin skips its own injection (it does not overwrite). Cross-
|
|
881
|
+
* tenant isolation is therefore only as strong as the caller's own
|
|
882
|
+
* stamping — same guarantee as a hand-rolled `skipWhen`.
|
|
815
883
|
*/
|
|
816
884
|
allowDataInjection?: boolean;
|
|
817
885
|
}
|
|
818
886
|
declare function multiTenantPlugin(options?: MultiTenantOptions): Plugin;
|
|
887
|
+
/**
|
|
888
|
+
* Build the canonical `skipWhen` callback for role-based tenant
|
|
889
|
+
* bypass — replaces the hand-rolled
|
|
890
|
+
* `(ctx) => ctx.role === 'superadmin'` callback that ~6 packages
|
|
891
|
+
* across the classytic codebase carry as boilerplate. Eliminates the
|
|
892
|
+
* "I forgot to include 'platform_admin'" drift class — the role
|
|
893
|
+
* vocabulary lives in one place.
|
|
894
|
+
*
|
|
895
|
+
* **Composes with `bypassTenant: true` per-call**, doesn't replace
|
|
896
|
+
* it. `bypassTenant` is the deliberate per-call form for migration
|
|
897
|
+
* scripts and one-off admin queries; `adminBypass` is the
|
|
898
|
+
* always-on role-based bypass for users whose JWT / session carries
|
|
899
|
+
* a privileged role.
|
|
900
|
+
*
|
|
901
|
+
* **NOT a substitute for authentication or RBAC.** This factory only
|
|
902
|
+
* answers "should the tenant scope hook fire for this call?" — it
|
|
903
|
+
* does NOT validate that the caller is authenticated, that their
|
|
904
|
+
* role claim is genuine, or that the requested operation is
|
|
905
|
+
* permitted. Always run auth + RBAC before the call reaches the
|
|
906
|
+
* repository layer; this is the LAST gate, not the only one.
|
|
907
|
+
*
|
|
908
|
+
* @example Single role
|
|
909
|
+
* ```ts
|
|
910
|
+
* import { adminBypass, multiTenantPlugin } from '@classytic/mongokit';
|
|
911
|
+
*
|
|
912
|
+
* const repo = new Repository(InvoiceModel, [
|
|
913
|
+
* multiTenantPlugin({
|
|
914
|
+
* tenantField: 'organizationId',
|
|
915
|
+
* skipWhen: adminBypass({ adminRoles: ['superadmin'] }),
|
|
916
|
+
* }),
|
|
917
|
+
* ]);
|
|
918
|
+
*
|
|
919
|
+
* await repo.getAll({}, { role: 'superadmin' }); // sees all orgs
|
|
920
|
+
* await repo.getAll({}, { role: 'user', organizationId: 'org-1' }); // scoped
|
|
921
|
+
* ```
|
|
922
|
+
*
|
|
923
|
+
* @example Multiple admin roles + custom field name
|
|
924
|
+
* ```ts
|
|
925
|
+
* skipWhen: adminBypass({
|
|
926
|
+
* roleField: 'principalRole', // ctx.principalRole, not ctx.role
|
|
927
|
+
* adminRoles: ['superadmin', 'platform_admin', 'support'],
|
|
928
|
+
* }),
|
|
929
|
+
* ```
|
|
930
|
+
*
|
|
931
|
+
* @example Composing with custom logic
|
|
932
|
+
* ```ts
|
|
933
|
+
* // adminBypass for the role check, then your own additional logic:
|
|
934
|
+
* const adminCheck = adminBypass({ adminRoles: ['superadmin'] });
|
|
935
|
+
* skipWhen: (ctx, op) => adminCheck(ctx, op) || ctx.internalSystemFlag === true,
|
|
936
|
+
* ```
|
|
937
|
+
*
|
|
938
|
+
* @param options.roleField - Path on the context to read the role
|
|
939
|
+
* from. Defaults to `'role'`. Top-level keys only — for nested
|
|
940
|
+
* shapes (e.g. `ctx.user.role`), wrap your own `(ctx) => …` callback
|
|
941
|
+
* instead.
|
|
942
|
+
* @param options.adminRoles - Role values that grant the bypass. The
|
|
943
|
+
* factory does an exact-match `includes` check — case-sensitive,
|
|
944
|
+
* no fuzzy matching. Lowercase your role vocabulary upstream.
|
|
945
|
+
* @returns A `skipWhen`-compatible callback `(ctx, op) → boolean`.
|
|
946
|
+
*/
|
|
947
|
+
declare function adminBypass(options: {
|
|
948
|
+
roleField?: string;
|
|
949
|
+
adminRoles: readonly string[];
|
|
950
|
+
}): (context: RepositoryContext, operation: string) => boolean;
|
|
819
951
|
//#endregion
|
|
820
952
|
//#region src/plugins/observability.plugin.d.ts
|
|
821
953
|
interface OperationMetric {
|
|
@@ -1088,4 +1220,4 @@ declare function immutableField(field: string): ValidatorDefinition;
|
|
|
1088
1220
|
*/
|
|
1089
1221
|
declare function uniqueField(field: string, errorMessage?: string): ValidatorDefinition;
|
|
1090
1222
|
//#endregion
|
|
1091
|
-
export {
|
|
1223
|
+
export { AuditQueryResult as $, elasticSearchPlugin as A, sequentialId as B, MethodRegistryRepository as C, leasePlugin as D, LeasePluginOptions as E, SequentialIdOptions as F, RepositoryCachePluginOptions as G, CacheAdapter$1 as H, customIdPlugin as I, BulkWriteResult as J, cachePlugin as K, dateSequentialId as L, DateSequentialIdOptions as M, IdGenerator as N, fieldFilterPlugin as O, PrefixedIdOptions as P, AuditQueryOptions as Q, getNextSequence as R, mongoOperationsPlugin as S, LeaseMethods as T, CacheOptions as U, cascadePlugin as V, RepositoryCacheHandle as W, AuditEntry as X, batchOperationsPlugin as Y, AuditOperation as Z, observabilityPlugin as _, uniqueField as a, AggregateHelpersMethods as at, multiTenantPlugin as b, TenantContext as c, SubdocumentMethods as d, AuditTrailMethods as et, subdocumentPlugin as f, OperationMetric as g, ObservabilityOptions as h, requireField as i, auditLogPlugin as it, CustomIdOptions as j, ElasticSearchOptions as k, TenantStore as l, softDeletePlugin as m, blockIf as n, AuditTrailQuery as nt, validationChainPlugin as o, aggregateHelpersPlugin as ot, SoftDeleteMethods as p, BatchOperationsMethods as q, immutableField as r, auditTrailPlugin as rt, timestampPlugin as s, autoInject as t, AuditTrailOptions as tt, createTenantContext as u, MultiTenantOptions as v, methodRegistryPlugin as w, MongoOperationsMethods as x, adminBypass as y, prefixedId as z };
|