@classytic/mongokit 3.38.1 → 3.40.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/dist/{PaginationEngine-CBS1XGzp.d.mts → PaginationEngine-D-P19Y9m.d.mts} +34 -9
- package/dist/PaginationEngine-Dvl8jcRY.mjs +1198 -0
- package/dist/{Repository-Ck5x43wF.mjs → Repository-BMgPSkqZ.mjs} +672 -655
- package/dist/{Repository-BdSkQ8vc.d.mts → Repository-CR8Y9g2E.d.mts} +106 -34
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +21 -0
- package/dist/adapter/index.mjs +1 -1
- package/dist/{adapter-Bfqz8yHE.mjs → adapter-BrVfPFSW.mjs} +45 -1
- package/dist/ai/index.d.mts +1 -1
- package/dist/better-auth/index.mjs +2 -2
- package/dist/{create-repository-B5EulQ31.mjs → create-repository-BzBUMvd1.mjs} +3 -3
- package/dist/{create-repository-BU4x9Kb0.d.mts → create-repository-CAle1MSU.d.mts} +5 -5
- package/dist/idempotency/index.d.mts +23 -0
- package/dist/idempotency/index.mjs +134 -0
- package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
- package/dist/index.d.mts +125 -35
- package/dist/index.mjs +286 -676
- package/dist/indexes/verify-indexes.d.mts +14 -1
- package/dist/indexes/verify-indexes.mjs +54 -7
- package/dist/{method-registry.plugin-DblM7V6j.mjs → method-registry.plugin-C63pGn1d.mjs} +54 -4
- package/dist/{multi-tenant.plugin-BwSvTirB.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +77 -5
- package/dist/{operations-DZodvjC-.d.mts → operations-CrRIoc8Z.d.mts} +22 -7
- package/dist/outbox/index.d.mts +1 -1
- package/dist/outbox/index.mjs +7 -5
- package/dist/pagination/PaginationEngine.d.mts +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +1 -1
- package/dist/pagination/utils/limits.mjs +22 -1
- package/dist/pagination-BpIv2bdS.d.mts +364 -0
- package/dist/{plugin-options-DXnlKM3B.d.mts → plugin-options-Zk46lD6F.d.mts} +16 -2
- package/dist/plugins/index.d.mts +8 -3
- package/dist/plugins/index.mjs +99 -26
- package/dist/{repository-H_ffGgpE.d.mts → repository-odl03582.d.mts} +50 -6
- package/dist/sync/index.d.mts +40 -0
- package/dist/sync/index.mjs +334 -0
- package/dist/tenant/index.d.mts +89 -0
- package/dist/tenant/index.mjs +96 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-DF0gJB6p.mjs} +116 -9
- package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
- package/dist/transaction-B5L9glO0.mjs +533 -0
- package/dist/{update-jSXp3nu3.mjs → update-DG0fCwRJ.mjs} +293 -83
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
- package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +26 -20
- package/package.json +17 -4
- package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
- package/dist/core-62oo3N1C.d.mts +0 -50
- package/dist/pagination-jSUdrACK.d.mts +0 -158
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* Missing UNIQUE indexes are reported separately from missing regular ones —
|
|
15
15
|
* the first silently admits duplicate data, the second only costs a scan.
|
|
16
16
|
*
|
|
17
|
+
* Same keys are not the same index: a live index is compared on the options that decide
|
|
18
|
+
* correctness (`unique`, `partialFilterExpression`, `sparse`, `collation`, `expireAfterSeconds`).
|
|
19
|
+
* A same-key index that differs is reported as `incompatible` — its fix is a rebuild, not a create.
|
|
20
|
+
*
|
|
17
21
|
* DELIBERATELY ONE-DIRECTIONAL: an index present in the database but absent
|
|
18
22
|
* from a schema is NOT drift. Auth libraries and migrations manage indexes
|
|
19
23
|
* outside the ODM, and treating those as removable is exactly what makes a
|
|
@@ -44,11 +48,20 @@ interface MissingIndex {
|
|
|
44
48
|
/** The declared key, JSON-stringified — stable to log and to diff. */
|
|
45
49
|
key: string;
|
|
46
50
|
}
|
|
51
|
+
/** A live index on the declared keys whose correctness options differ from the declaration. */
|
|
52
|
+
interface IncompatibleIndex extends MissingIndex {
|
|
53
|
+
/** Declared `unique` — a mismatch here can already have admitted duplicates. */
|
|
54
|
+
unique: boolean;
|
|
55
|
+
/** One line per differing option: `unique: declared true, live false`. */
|
|
56
|
+
differences: string[];
|
|
57
|
+
}
|
|
47
58
|
interface IndexVerifyReport {
|
|
48
59
|
modelsChecked: number;
|
|
49
60
|
/** Declared `unique` indexes with nothing behind them. Duplicates can already exist. */
|
|
50
61
|
missingUnique: MissingIndex[];
|
|
51
62
|
missingRegular: MissingIndex[];
|
|
63
|
+
/** Same keys, different semantics — e.g. declared unique, built non-unique. */
|
|
64
|
+
incompatible: IncompatibleIndex[];
|
|
52
65
|
/** Models whose indexes could not be read — collection absent, or no permission. */
|
|
53
66
|
unreadable: string[];
|
|
54
67
|
}
|
|
@@ -66,4 +79,4 @@ declare function verifyIndexes(connection: IndexVerifiableConnection, options?:
|
|
|
66
79
|
/** One-line summary for a boot log. */
|
|
67
80
|
declare function formatIndexReport(report: IndexVerifyReport): string;
|
|
68
81
|
//#endregion
|
|
69
|
-
export { IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
|
|
82
|
+
export { IncompatibleIndex, IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
|
|
@@ -22,12 +22,45 @@ function textFields(key) {
|
|
|
22
22
|
const fields = Object.entries(key).filter(([, dir]) => dir === "text").map(([field]) => field);
|
|
23
23
|
return fields.length > 0 ? fields.sort() : null;
|
|
24
24
|
}
|
|
25
|
+
/** Order-free JSON, so `{a:1,b:2}` equals `{b:2,a:1}` — option objects are not order-sensitive. */
|
|
26
|
+
function canonical(value) {
|
|
27
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
|
|
28
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
29
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The options that change what an index GUARANTEES, compared declared → live. Absent means
|
|
33
|
+
* off (`unique`, `sparse`) or none (`partialFilterExpression`, `expireAfterSeconds`). A collation
|
|
34
|
+
* is compared on the DECLARED fields only: the server fills in every default it does not store.
|
|
35
|
+
*/
|
|
36
|
+
function optionDifferences(declared, live) {
|
|
37
|
+
const diffs = [];
|
|
38
|
+
const flag = (name) => {
|
|
39
|
+
const d = declared[name] === true;
|
|
40
|
+
const l = live[name] === true;
|
|
41
|
+
if (d !== l) diffs.push(`${name}: declared ${d}, live ${l}`);
|
|
42
|
+
};
|
|
43
|
+
flag("unique");
|
|
44
|
+
flag("sparse");
|
|
45
|
+
for (const name of ["partialFilterExpression", "expireAfterSeconds"]) {
|
|
46
|
+
const d = declared[name] === void 0 ? "none" : canonical(declared[name]);
|
|
47
|
+
const l = live[name] === void 0 ? "none" : canonical(live[name]);
|
|
48
|
+
if (d !== l) diffs.push(`${name}: declared ${d}, live ${l}`);
|
|
49
|
+
}
|
|
50
|
+
const dColl = declared.collation;
|
|
51
|
+
const lColl = live.collation;
|
|
52
|
+
if (dColl) {
|
|
53
|
+
if (Object.keys(dColl).filter((k) => canonical(dColl[k]) !== canonical(lColl?.[k])).length > 0) diffs.push(`collation: declared ${canonical(dColl)}, live ${lColl ? canonical(lColl) : "none"}`);
|
|
54
|
+
} else if (lColl && lColl.locale !== "simple") diffs.push(`collation: declared none, live ${canonical(lColl)}`);
|
|
55
|
+
return diffs;
|
|
56
|
+
}
|
|
25
57
|
async function verifyIndexes(connection, options = {}) {
|
|
26
58
|
const include = options.includeModel ?? (() => true);
|
|
27
59
|
const report = {
|
|
28
60
|
modelsChecked: 0,
|
|
29
61
|
missingUnique: [],
|
|
30
62
|
missingRegular: [],
|
|
63
|
+
incompatible: [],
|
|
31
64
|
unreadable: []
|
|
32
65
|
};
|
|
33
66
|
for (const name of connection.modelNames().sort()) {
|
|
@@ -39,8 +72,12 @@ async function verifyIndexes(connection, options = {}) {
|
|
|
39
72
|
let existingText;
|
|
40
73
|
try {
|
|
41
74
|
const live = await model.collection.listIndexes().toArray();
|
|
42
|
-
existing =
|
|
43
|
-
existingText =
|
|
75
|
+
existing = /* @__PURE__ */ new Map();
|
|
76
|
+
existingText = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const index of live) {
|
|
78
|
+
const [map, k] = index.weights ? [existingText, JSON.stringify(Object.keys(index.weights).sort())] : [existing, normalizeKey(index.key)];
|
|
79
|
+
map.set(k, [...map.get(k) ?? [], index]);
|
|
80
|
+
}
|
|
44
81
|
} catch {
|
|
45
82
|
report.unreadable.push(name);
|
|
46
83
|
continue;
|
|
@@ -48,15 +85,24 @@ async function verifyIndexes(connection, options = {}) {
|
|
|
48
85
|
report.modelsChecked += 1;
|
|
49
86
|
for (const [key, indexOptions] of declared) {
|
|
50
87
|
const asText = textFields(key);
|
|
51
|
-
|
|
52
|
-
if (existingText.has(JSON.stringify(asText))) continue;
|
|
53
|
-
} else if (existing.has(normalizeKey(key))) continue;
|
|
88
|
+
const sameKey = asText ? existingText.get(JSON.stringify(asText)) : existing.get(normalizeKey(key));
|
|
54
89
|
const entry = {
|
|
55
90
|
model: name,
|
|
56
91
|
collection: model.collection.name,
|
|
57
92
|
key: JSON.stringify(key)
|
|
58
93
|
};
|
|
59
|
-
|
|
94
|
+
const opts = indexOptions ?? {};
|
|
95
|
+
if (sameKey && sameKey.length > 0) {
|
|
96
|
+
const diffsPerCandidate = sameKey.map((live) => optionDifferences(opts, live));
|
|
97
|
+
if (diffsPerCandidate.some((d) => d.length === 0)) continue;
|
|
98
|
+
report.incompatible.push({
|
|
99
|
+
...entry,
|
|
100
|
+
unique: opts.unique === true,
|
|
101
|
+
differences: diffsPerCandidate[0] ?? []
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (opts.unique === true) report.missingUnique.push(entry);
|
|
60
106
|
else report.missingRegular.push(entry);
|
|
61
107
|
}
|
|
62
108
|
}
|
|
@@ -67,7 +113,8 @@ function formatIndexReport(report) {
|
|
|
67
113
|
const parts = [
|
|
68
114
|
`${report.modelsChecked} models checked`,
|
|
69
115
|
`missing unique: ${report.missingUnique.length}`,
|
|
70
|
-
`missing regular: ${report.missingRegular.length}
|
|
116
|
+
`missing regular: ${report.missingRegular.length}`,
|
|
117
|
+
`incompatible: ${report.incompatible.length}`
|
|
71
118
|
];
|
|
72
119
|
if (report.unreadable.length > 0) parts.push(`unreadable: ${report.unreadable.length}`);
|
|
73
120
|
return `[index-check] ${parts.join(" — ")}`;
|
|
@@ -16,6 +16,58 @@ import { t as createError } from "./error-BuByZwKU.mjs";
|
|
|
16
16
|
* // bulkWrite is what this plugin contributes.
|
|
17
17
|
* await repo.bulkWrite([...ops]);
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Driver options `bulkWrite` forwards, and the only ones it forwards.
|
|
21
|
+
*
|
|
22
|
+
* The signature used to be `{ session?, ordered?, [key: string]: unknown }` and
|
|
23
|
+
* the call forwarded exactly two of them. So `writeConcern: { w: 'majority' }`
|
|
24
|
+
* on a money import was accepted by the type, dropped on the floor, and the
|
|
25
|
+
* write went out under the default concern with nothing raised.
|
|
26
|
+
*/
|
|
27
|
+
const BULK_WRITE_OPTIONS = [
|
|
28
|
+
"ordered",
|
|
29
|
+
"session",
|
|
30
|
+
"writeConcern",
|
|
31
|
+
"bypassDocumentValidation",
|
|
32
|
+
"comment",
|
|
33
|
+
"let",
|
|
34
|
+
"ignoreUndefined"
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Driver options that EXIST but this method does not forward.
|
|
38
|
+
*
|
|
39
|
+
* The options bag is dual-purpose by design: it is spread into
|
|
40
|
+
* `_buildContext`, so it also carries plugin context — `organizationId` for
|
|
41
|
+
* multi-tenant scoping, and whatever else a host's plugins read off it.
|
|
42
|
+
* Refusing every unrecognised key therefore breaks legitimate callers, which
|
|
43
|
+
* is exactly how the first version of this fence failed: it rejected the
|
|
44
|
+
* tenant id and took two passing tests with it.
|
|
45
|
+
*
|
|
46
|
+
* So the line sits where the caller's expectation is. If the DRIVER would
|
|
47
|
+
* understand the option and we would silently not pass it on, refuse — that is
|
|
48
|
+
* the case where someone believes they configured the write. A key the driver
|
|
49
|
+
* does not know either is plugin context, and passes through untouched.
|
|
50
|
+
*/
|
|
51
|
+
const UNSUPPORTED_DRIVER_OPTIONS = /* @__PURE__ */ new Set([
|
|
52
|
+
"forceServerObjectId",
|
|
53
|
+
"timeoutMS",
|
|
54
|
+
"maxTimeMS",
|
|
55
|
+
"retryWrites",
|
|
56
|
+
"checkKeys",
|
|
57
|
+
"serializeFunctions",
|
|
58
|
+
"enableUtf8Validation",
|
|
59
|
+
"raw",
|
|
60
|
+
"explain"
|
|
61
|
+
]);
|
|
62
|
+
/** Narrow the bag to driver options, refusing the ones we would silently drop. */
|
|
63
|
+
function pickBulkWriteOptions(options) {
|
|
64
|
+
const dropped = Object.keys(options).filter((k) => UNSUPPORTED_DRIVER_OPTIONS.has(k));
|
|
65
|
+
if (dropped.length > 0) throw createError(400, `bulkWrite: option(s) ${dropped.map((k) => `'${k}'`).join(", ")} are MongoDB driver options this method does not forward. Refused rather than ignored, because silently dropping one changes what the write does. Supported: ${BULK_WRITE_OPTIONS.join(", ")}.`);
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const key of BULK_WRITE_OPTIONS) if (options[key] !== void 0) out[key] = options[key];
|
|
68
|
+
out.ordered = options.ordered ?? true;
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
19
71
|
function batchOperationsPlugin() {
|
|
20
72
|
return {
|
|
21
73
|
name: "batch-operations",
|
|
@@ -36,6 +88,7 @@ function batchOperationsPlugin() {
|
|
|
36
88
|
* ]);
|
|
37
89
|
*/
|
|
38
90
|
repo.registerMethod("bulkWrite", async function(operations, options = {}) {
|
|
91
|
+
const driverOptions = pickBulkWriteOptions(options);
|
|
39
92
|
const context = await this._buildContext("bulkWrite", {
|
|
40
93
|
operations,
|
|
41
94
|
...options
|
|
@@ -43,10 +96,7 @@ function batchOperationsPlugin() {
|
|
|
43
96
|
try {
|
|
44
97
|
const finalOps = context.operations || operations;
|
|
45
98
|
if (!finalOps || finalOps.length === 0) throw createError(400, "bulkWrite requires at least one operation");
|
|
46
|
-
const result = await this.Model.bulkWrite(finalOps,
|
|
47
|
-
ordered: options.ordered ?? true,
|
|
48
|
-
session: options.session
|
|
49
|
-
});
|
|
99
|
+
const result = await this.Model.bulkWrite(finalOps, driverOptions);
|
|
50
100
|
const bulkResult = {
|
|
51
101
|
ok: result.ok,
|
|
52
102
|
insertedCount: result.insertedCount,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { l as Plugin, p as RepositoryContext } from "./repository-
|
|
1
|
+
import { g as ObjectId } from "./pagination-BpIv2bdS.mjs";
|
|
2
|
+
import { l as Plugin, p as RepositoryContext } from "./repository-odl03582.mjs";
|
|
3
3
|
import mongoose, { Connection } from "mongoose";
|
|
4
4
|
import { CacheAdapter as CacheAdapter$1, CacheOptions, RepositoryCacheHandle as RepositoryCacheHandle$1, RepositoryCachePluginOptions, cachePlugin } from "@classytic/repo-core/cache";
|
|
5
5
|
import { TenantConfig } from "@classytic/repo-core/tenant";
|
|
@@ -307,12 +307,31 @@ interface CustomIdOptions {
|
|
|
307
307
|
* });
|
|
308
308
|
*/
|
|
309
309
|
declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
|
|
310
|
+
/**
|
|
311
|
+
* Current value of a counter, or `null` when it has never been created.
|
|
312
|
+
* `null` and `0` differ: only a missing counter needs seeding.
|
|
313
|
+
*/
|
|
314
|
+
declare function readSequence(counterKey: string, connection?: mongoose.Connection): Promise<number | null>;
|
|
315
|
+
/**
|
|
316
|
+
* Raise a counter to at least `floor` — never lowers it. Atomic (`$max` +
|
|
317
|
+
* upsert), so concurrent seeders converge on the highest floor.
|
|
318
|
+
*
|
|
319
|
+
* Use before a counter's first increment when ids were already issued under
|
|
320
|
+
* another key (e.g. moving from a global to a per-tenant sequence); otherwise
|
|
321
|
+
* the new counter restarts at 1 and re-issues taken ids.
|
|
322
|
+
*
|
|
323
|
+
* @returns The counter's value after the raise.
|
|
324
|
+
*/
|
|
325
|
+
declare function ensureSequenceAtLeast(counterKey: string, floor: number, connection?: mongoose.Connection): Promise<number>;
|
|
310
326
|
interface SequentialIdOptions {
|
|
311
327
|
/** Prefix string (e.g., 'INV', 'ORD') */
|
|
312
328
|
prefix: string;
|
|
313
329
|
/** Mongoose model — used to derive the counter key from model name */
|
|
314
330
|
model: mongoose.Model<any>;
|
|
315
|
-
/**
|
|
331
|
+
/**
|
|
332
|
+
* MINIMUM number of digits (default: 4 → "0001"). Never truncates —
|
|
333
|
+
* sequence 10000 under `padding: 4` renders as `INV-10000`.
|
|
334
|
+
*/
|
|
316
335
|
padding?: number;
|
|
317
336
|
/** Separator between prefix and number (default: '-') */
|
|
318
337
|
separator?: string;
|
|
@@ -346,10 +365,54 @@ interface DateSequentialIdOptions {
|
|
|
346
365
|
* - 'daily' → BILL-2026-02-20-0001, resets every day
|
|
347
366
|
*/
|
|
348
367
|
partition?: 'yearly' | 'monthly' | 'daily';
|
|
349
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* MINIMUM number of digits for the sequence (default: 4). `padStart` never
|
|
370
|
+
* truncates, so the 10,000th id of a period under `padding: 4` is
|
|
371
|
+
* `BILL-2026-10000` — it grows a digit rather than wrapping to `0000`.
|
|
372
|
+
* Lexical sort order therefore only holds while a period stays below
|
|
373
|
+
* 10^padding; size the padding for your peak period.
|
|
374
|
+
*/
|
|
350
375
|
padding?: number;
|
|
351
376
|
/** Separator (default: '-') */
|
|
352
377
|
separator?: string;
|
|
378
|
+
/**
|
|
379
|
+
* Counter scope.
|
|
380
|
+
* - `'global'` (default) — one counter per model + period, shared by
|
|
381
|
+
* every tenant in the database. Ids are unique across the deployment.
|
|
382
|
+
* - `'tenant'` — one counter per tenant + model + period, keyed by
|
|
383
|
+
* `context[tenantKey]`. Every tenant's numbering starts at 1 (Shopify-
|
|
384
|
+
* style per-shop order numbers) and the write hot spot on the single
|
|
385
|
+
* shared counter document splits per tenant. Ids are unique WITHIN a
|
|
386
|
+
* tenant only — index the field compound with the tenant field.
|
|
387
|
+
* Fails closed: a create with no tenant in context throws rather than
|
|
388
|
+
* falling back to the global key, which would silently merge two
|
|
389
|
+
* tenants' sequences.
|
|
390
|
+
*/
|
|
391
|
+
scope?: 'global' | 'tenant';
|
|
392
|
+
/**
|
|
393
|
+
* Context key carrying the tenant id under `scope: 'tenant'` (default:
|
|
394
|
+
* `'organizationId'` — the same default as `multiTenantPlugin`'s
|
|
395
|
+
* `contextKey`, so `repo.create(data, { organizationId })` feeds both).
|
|
396
|
+
*/
|
|
397
|
+
tenantKey?: string;
|
|
398
|
+
/**
|
|
399
|
+
* Which clock decides the period boundary.
|
|
400
|
+
*
|
|
401
|
+
* - `'local'` (default) — the server's `TZ`. Kept as the default because it
|
|
402
|
+
* is what every existing deployment already generated: flipping it would
|
|
403
|
+
* retroactively change which period an id near a month boundary belongs
|
|
404
|
+
* to, and those ids are invoice and order numbers.
|
|
405
|
+
* - `'utc'` — **the correct choice for more than one replica.** Two replicas
|
|
406
|
+
* with different `TZ` settings derive different counter keys for the same
|
|
407
|
+
* logical month and each increments its own, so the same number is issued
|
|
408
|
+
* twice inside one tenant-period. UTC is the one boundary every replica
|
|
409
|
+
* agrees on. It also matches the ecosystem's other period key,
|
|
410
|
+
* `usagePeriod()` in `@classytic/repo-core/usage`.
|
|
411
|
+
*
|
|
412
|
+
* Containers usually run UTC anyway, which is exactly why the difference
|
|
413
|
+
* stays invisible until one host is configured otherwise.
|
|
414
|
+
*/
|
|
415
|
+
timezone?: 'local' | 'utc';
|
|
353
416
|
}
|
|
354
417
|
/**
|
|
355
418
|
* Generator: Date-partitioned sequential counter.
|
|
@@ -371,6 +434,15 @@ interface DateSequentialIdOptions {
|
|
|
371
434
|
* }),
|
|
372
435
|
* })
|
|
373
436
|
* ```
|
|
437
|
+
*
|
|
438
|
+
* @example Per-tenant numbering in a shared database
|
|
439
|
+
* ```typescript
|
|
440
|
+
* customIdPlugin({
|
|
441
|
+
* field: 'orderNumber',
|
|
442
|
+
* generator: dateSequentialId({ prefix: 'ORD', model: OrderModel, scope: 'tenant' }),
|
|
443
|
+
* })
|
|
444
|
+
* // org-a: ORD-2026-02-0001, ORD-2026-02-0002 … org-b: ORD-2026-02-0001 …
|
|
445
|
+
* ```
|
|
374
446
|
*/
|
|
375
447
|
declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
|
|
376
448
|
interface PrefixedIdOptions {
|
|
@@ -582,4 +654,4 @@ declare function adminBypass(options: {
|
|
|
582
654
|
adminRoles: readonly string[];
|
|
583
655
|
}): (context: RepositoryContext, operation: string) => boolean;
|
|
584
656
|
//#endregion
|
|
585
|
-
export {
|
|
657
|
+
export { EnsureAuditTrailReadyOptions as A, AuditQueryOptions as C, AuditTrailOptions as D, AuditTrailMode as E, ensureAuditTrailReady as M, AuditTrailQuery as O, AuditOperation as S, AuditTrailMethods as T, CacheOptions as _, DateSequentialIdOptions as a, cachePlugin as b, SequentialIdOptions as c, ensureSequenceAtLeast as d, getNextSequence as f, CacheAdapter$1 as g, sequentialId as h, CustomIdOptions as i, auditTrailPlugin as j, AuditTrailQueryOptions as k, customIdPlugin as l, readSequence as m, adminBypass as n, IdGenerator as o, prefixedId as p, multiTenantPlugin as r, PrefixedIdOptions as s, MultiTenantOptions as t, dateSequentialId as u, RepositoryCacheHandle$1 as v, AuditQueryResult as w, AuditEntry as x, RepositoryCachePluginOptions as y };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as CollationOptions } from "./pagination-jSUdrACK.mjs";
|
|
1
|
+
import { _ as PopulateSpec, g as ObjectId, i as CountStrategy, r as CollationOptions, v as ReadPreferenceType, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
|
|
3
2
|
import { DeleteManyResult, DeleteResult, LookupPopulateResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
4
3
|
import { PipelineStage, PopulateOptions } from "mongoose";
|
|
5
4
|
import { Filter } from "@classytic/repo-core/filter";
|
|
@@ -383,9 +382,21 @@ interface WithTransactionOptions {
|
|
|
383
382
|
}
|
|
384
383
|
/** Create operation options */
|
|
385
384
|
interface CreateOptions extends SessionOptions {
|
|
386
|
-
/**
|
|
387
|
-
*
|
|
388
|
-
*
|
|
385
|
+
/**
|
|
386
|
+
* Keep insertion order on error. **Default `false`, which is the OPPOSITE of
|
|
387
|
+
* the MongoDB driver's own default** — a deliberate choice for import
|
|
388
|
+
* throughput, pinned by `tests/integration/createMany-partial-failure.test.ts`.
|
|
389
|
+
*
|
|
390
|
+
* The consequence is worth stating plainly, because it is the part that
|
|
391
|
+
* surprises: with `false`, a REJECTED `createMany` can still have written
|
|
392
|
+
* documents. Every valid document inserts and only the failures are reported,
|
|
393
|
+
* so a caller that treats the rejection as "nothing happened" and retries
|
|
394
|
+
* will double-insert everything that succeeded the first time.
|
|
395
|
+
*
|
|
396
|
+
* Retryable imports therefore need a unique index or an idempotency key —
|
|
397
|
+
* not a try/catch. Pass `ordered: true` when the batch is an all-or-nothing
|
|
398
|
+
* business operation, and a transaction when it must also be atomic.
|
|
399
|
+
*/
|
|
389
400
|
ordered?: boolean;
|
|
390
401
|
}
|
|
391
402
|
/** Update operation options */
|
|
@@ -485,14 +496,18 @@ interface LookupPopulateOptions<TBase = unknown> {
|
|
|
485
496
|
page?: number;
|
|
486
497
|
/** Cursor token for next page (keyset mode) */
|
|
487
498
|
after?: string;
|
|
499
|
+
/** Cursor token for the PREVIOUS page (keyset mode). Mutually exclusive with `after`. */
|
|
500
|
+
before?: string;
|
|
488
501
|
/** Number of documents per page */
|
|
489
502
|
limit?: number;
|
|
490
503
|
/** Fields to select */
|
|
491
504
|
select?: SelectSpec;
|
|
492
505
|
/** Collation for locale-aware string comparison */
|
|
493
506
|
collation?: CollationOptions;
|
|
494
|
-
/** Count strategy for offset pagination */
|
|
495
|
-
countStrategy?:
|
|
507
|
+
/** Count strategy for offset pagination. See {@link CountStrategy}. */
|
|
508
|
+
countStrategy?: CountStrategy;
|
|
509
|
+
/** Ceiling for `countStrategy: 'capped'` (default 10000). */
|
|
510
|
+
countLimit?: number;
|
|
496
511
|
/**
|
|
497
512
|
* Transaction session. Typed `unknown` to match repo-core's
|
|
498
513
|
* `RepositorySession` contract — mongokit narrows to `ClientSession`
|
package/dist/outbox/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as Repository } from "../Repository-CR8Y9g2E.mjs";
|
|
2
2
|
import { Connection, Model } from "mongoose";
|
|
3
3
|
//#region src/outbox/index.d.ts
|
|
4
4
|
/** Delivery states. `dead_letter` is terminal and operator-visible. */
|
package/dist/outbox/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
2
|
-
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-
|
|
1
|
+
import { n as Repository } from "../Repository-BMgPSkqZ.mjs";
|
|
2
|
+
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-C63pGn1d.mjs";
|
|
3
3
|
import { Schema } from "mongoose";
|
|
4
4
|
//#region src/outbox/index.ts
|
|
5
5
|
/**
|
|
@@ -172,12 +172,14 @@ function createOutboxModel(connection, options = {}) {
|
|
|
172
172
|
*
|
|
173
173
|
* Mongo rejects `unique` combined with a `sparse: true` + partial filter ("cannot mix"), and
|
|
174
174
|
* the failure is that the index silently never builds — so duplicate events insert happily
|
|
175
|
-
* and the outbox's idempotency guarantee is gone with nothing in the logs.
|
|
176
|
-
*
|
|
175
|
+
* and the outbox's idempotency guarantee is gone with nothing in the logs. `$gte: ''` indexes
|
|
176
|
+
* only rows that carry a string key — the same rows as `$type: 'string'`, but the dedupe
|
|
177
|
+
* lookup by key can use it (a `$type` filter is never implied by an equality query).
|
|
177
178
|
*/
|
|
178
179
|
schema.index({ dedupeKey: 1 }, {
|
|
180
|
+
name: "dedupeKey_str",
|
|
179
181
|
unique: true,
|
|
180
|
-
partialFilterExpression: { dedupeKey: { $
|
|
182
|
+
partialFilterExpression: { dedupeKey: { $gte: "" } }
|
|
181
183
|
});
|
|
182
184
|
/**
|
|
183
185
|
* The DEAD-LETTER read. `getDeadLettered` is part of the `OutboxStore` contract and is what
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-D-P19Y9m.mjs";
|
|
2
2
|
export { PaginationEngine };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-Dvl8jcRY.mjs";
|
|
2
2
|
export { PaginationEngine };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as createError } from "../../error-BuByZwKU.mjs";
|
|
1
2
|
//#region src/pagination/utils/limits.ts
|
|
2
3
|
/**
|
|
3
4
|
* Validates and sanitizes limit value
|
|
@@ -27,7 +28,27 @@ function validatePage(page, config) {
|
|
|
27
28
|
const parsed = Number(page);
|
|
28
29
|
if (!Number.isFinite(parsed) || parsed < 1) return 1;
|
|
29
30
|
const sanitized = Math.floor(parsed);
|
|
30
|
-
|
|
31
|
+
const max = config.maxPage || 1e4;
|
|
32
|
+
if (sanitized > max)
|
|
33
|
+
/**
|
|
34
|
+
* 400, not a bare `Error`.
|
|
35
|
+
*
|
|
36
|
+
* The page number comes from the querystring, so this is the caller's
|
|
37
|
+
* mistake — a crawler walking past the end, a fuzzer, a stale deep link.
|
|
38
|
+
* A bare `Error` carries no status, so every framework above maps it to
|
|
39
|
+
* `internal_error` 500: the request is refused either way, but one of them
|
|
40
|
+
* pages an on-call engineer and burns an error budget over `?page=999999`.
|
|
41
|
+
*
|
|
42
|
+
* It matters more the lower `maxPage` is set, and a deployment that has
|
|
43
|
+
* tuned it down for the skip cost is exactly the one getting this traffic.
|
|
44
|
+
*/
|
|
45
|
+
throw createError(400, `Page ${sanitized} exceeds maximum ${max}`, {
|
|
46
|
+
code: "PAGE_OUT_OF_RANGE",
|
|
47
|
+
meta: {
|
|
48
|
+
page: sanitized,
|
|
49
|
+
maxPage: max
|
|
50
|
+
}
|
|
51
|
+
});
|
|
31
52
|
return sanitized;
|
|
32
53
|
}
|
|
33
54
|
/**
|