@classytic/mongokit 3.38.1 → 3.39.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-DQ7beTCR.mjs +1182 -0
- package/dist/{Repository-Ck5x43wF.mjs → Repository-D7fDpyFx.mjs} +400 -37
- package/dist/{Repository-BdSkQ8vc.d.mts → Repository-IxI983_m.d.mts} +55 -8
- 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-BU4x9Kb0.d.mts → create-repository-CRXjDa3N.d.mts} +5 -5
- package/dist/{create-repository-B5EulQ31.mjs → create-repository-DOe9E_5F.mjs} +3 -3
- package/dist/{index-DKEql4cL.d.mts → index-CmjNyulu.d.mts} +67 -8
- package/dist/index.d.mts +77 -13
- package/dist/index.mjs +8 -8
- 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-B-tzNeYL.d.mts} +60 -4
- 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 +2 -2
- 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-BuvtOSZf.d.mts} +2 -2
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +3 -3
- package/dist/{repository-H_ffGgpE.d.mts → repository-ZbLtL7Nk.d.mts} +30 -4
- package/dist/tenant/index.d.mts +89 -0
- package/dist/tenant/index.mjs +94 -0
- package/dist/testkit/index.d.mts +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-CBQukQzV.mjs → timestamp.plugin-B5eiaItQ.mjs} +31 -8
- package/dist/{to-plain-Dyz_qvuo.d.mts → to-plain-B8ufk3Qz.d.mts} +2 -2
- package/dist/{update-jSXp3nu3.mjs → update-c_01A0if.mjs} +126 -73
- package/dist/utils/index.d.mts +1 -1
- package/dist/{validation-chain.plugin-DhNyxNCw.mjs → validation-chain.plugin-Bju1D1Sn.mjs} +2 -2
- package/dist/{validation-chain.plugin-yQRob3Av.d.mts → validation-chain.plugin-DvrLJIbJ.d.mts} +3 -19
- package/package.json +7 -3
- package/dist/PaginationEngine-BX9o_-4R.mjs +0 -735
- package/dist/core-62oo3N1C.d.mts +0 -50
- package/dist/pagination-jSUdrACK.d.mts +0 -158
|
@@ -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-ZbLtL7Nk.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";
|
|
@@ -312,7 +312,10 @@ interface SequentialIdOptions {
|
|
|
312
312
|
prefix: string;
|
|
313
313
|
/** Mongoose model — used to derive the counter key from model name */
|
|
314
314
|
model: mongoose.Model<any>;
|
|
315
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* MINIMUM number of digits (default: 4 → "0001"). Never truncates —
|
|
317
|
+
* sequence 10000 under `padding: 4` renders as `INV-10000`.
|
|
318
|
+
*/
|
|
316
319
|
padding?: number;
|
|
317
320
|
/** Separator between prefix and number (default: '-') */
|
|
318
321
|
separator?: string;
|
|
@@ -346,10 +349,54 @@ interface DateSequentialIdOptions {
|
|
|
346
349
|
* - 'daily' → BILL-2026-02-20-0001, resets every day
|
|
347
350
|
*/
|
|
348
351
|
partition?: 'yearly' | 'monthly' | 'daily';
|
|
349
|
-
/**
|
|
352
|
+
/**
|
|
353
|
+
* MINIMUM number of digits for the sequence (default: 4). `padStart` never
|
|
354
|
+
* truncates, so the 10,000th id of a period under `padding: 4` is
|
|
355
|
+
* `BILL-2026-10000` — it grows a digit rather than wrapping to `0000`.
|
|
356
|
+
* Lexical sort order therefore only holds while a period stays below
|
|
357
|
+
* 10^padding; size the padding for your peak period.
|
|
358
|
+
*/
|
|
350
359
|
padding?: number;
|
|
351
360
|
/** Separator (default: '-') */
|
|
352
361
|
separator?: string;
|
|
362
|
+
/**
|
|
363
|
+
* Counter scope.
|
|
364
|
+
* - `'global'` (default) — one counter per model + period, shared by
|
|
365
|
+
* every tenant in the database. Ids are unique across the deployment.
|
|
366
|
+
* - `'tenant'` — one counter per tenant + model + period, keyed by
|
|
367
|
+
* `context[tenantKey]`. Every tenant's numbering starts at 1 (Shopify-
|
|
368
|
+
* style per-shop order numbers) and the write hot spot on the single
|
|
369
|
+
* shared counter document splits per tenant. Ids are unique WITHIN a
|
|
370
|
+
* tenant only — index the field compound with the tenant field.
|
|
371
|
+
* Fails closed: a create with no tenant in context throws rather than
|
|
372
|
+
* falling back to the global key, which would silently merge two
|
|
373
|
+
* tenants' sequences.
|
|
374
|
+
*/
|
|
375
|
+
scope?: 'global' | 'tenant';
|
|
376
|
+
/**
|
|
377
|
+
* Context key carrying the tenant id under `scope: 'tenant'` (default:
|
|
378
|
+
* `'organizationId'` — the same default as `multiTenantPlugin`'s
|
|
379
|
+
* `contextKey`, so `repo.create(data, { organizationId })` feeds both).
|
|
380
|
+
*/
|
|
381
|
+
tenantKey?: string;
|
|
382
|
+
/**
|
|
383
|
+
* Which clock decides the period boundary.
|
|
384
|
+
*
|
|
385
|
+
* - `'local'` (default) — the server's `TZ`. Kept as the default because it
|
|
386
|
+
* is what every existing deployment already generated: flipping it would
|
|
387
|
+
* retroactively change which period an id near a month boundary belongs
|
|
388
|
+
* to, and those ids are invoice and order numbers.
|
|
389
|
+
* - `'utc'` — **the correct choice for more than one replica.** Two replicas
|
|
390
|
+
* with different `TZ` settings derive different counter keys for the same
|
|
391
|
+
* logical month and each increments its own, so the same number is issued
|
|
392
|
+
* twice inside one tenant-period. UTC is the one boundary every replica
|
|
393
|
+
* agrees on. It also matches the ecosystem's other period key,
|
|
394
|
+
* `usagePeriod()` in `@classytic/repo-core/usage`.
|
|
395
|
+
*
|
|
396
|
+
* Containers usually run UTC anyway, which is exactly why the difference
|
|
397
|
+
* stays invisible until one host is configured otherwise.
|
|
398
|
+
*/
|
|
399
|
+
timezone?: 'local' | 'utc';
|
|
353
400
|
}
|
|
354
401
|
/**
|
|
355
402
|
* Generator: Date-partitioned sequential counter.
|
|
@@ -371,6 +418,15 @@ interface DateSequentialIdOptions {
|
|
|
371
418
|
* }),
|
|
372
419
|
* })
|
|
373
420
|
* ```
|
|
421
|
+
*
|
|
422
|
+
* @example Per-tenant numbering in a shared database
|
|
423
|
+
* ```typescript
|
|
424
|
+
* customIdPlugin({
|
|
425
|
+
* field: 'orderNumber',
|
|
426
|
+
* generator: dateSequentialId({ prefix: 'ORD', model: OrderModel, scope: 'tenant' }),
|
|
427
|
+
* })
|
|
428
|
+
* // org-a: ORD-2026-02-0001, ORD-2026-02-0002 … org-b: ORD-2026-02-0001 …
|
|
429
|
+
* ```
|
|
374
430
|
*/
|
|
375
431
|
declare function dateSequentialId(options: DateSequentialIdOptions): IdGenerator;
|
|
376
432
|
interface PrefixedIdOptions {
|
|
@@ -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 { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-IxI983_m.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-D7fDpyFx.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
|
/**
|
|
@@ -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-DQ7beTCR.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
|
/**
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Document, Model, PipelineStage, PopulateOptions, Types } from "mongoose";
|
|
2
|
+
//#region src/types/core.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Core query/document primitives shared across every mongokit module —
|
|
5
|
+
* sort/select/populate/filter specs and the mongoose-facing aliases.
|
|
6
|
+
*
|
|
7
|
+
* Also the home of the `softRequired` Mongoose declaration merge (this file
|
|
8
|
+
* is loaded by every module, so the augmentation is always in the program).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Global declaration merge: adds `softRequired` to Mongoose's SchemaTypeOptions
|
|
12
|
+
* so consumers can annotate paths type-safely without importing anything.
|
|
13
|
+
*
|
|
14
|
+
* A path with `softRequired: true` keeps its DB-level `required: true`
|
|
15
|
+
* invariant but is excluded from the auto-generated CRUD body `required[]`
|
|
16
|
+
* array (see buildCrudSchemasFromModel).
|
|
17
|
+
*/
|
|
18
|
+
declare module 'mongoose' {
|
|
19
|
+
interface SchemaTypeOptions<T, EnforcedDocType = any, THydratedDocumentType = HydratedDocument<EnforcedDocType>> {
|
|
20
|
+
/** mongokit: omit from auto-generated CRUD body `required` array. */
|
|
21
|
+
softRequired?: boolean;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Read Preference Type for replica sets */
|
|
25
|
+
type ReadPreferenceType = 'primary' | 'primaryPreferred' | 'secondary' | 'secondaryPreferred' | 'nearest' | (string & {});
|
|
26
|
+
/** Re-export mongoose ObjectId */
|
|
27
|
+
type ObjectId = Types.ObjectId;
|
|
28
|
+
/** Generic document type */
|
|
29
|
+
type AnyDocument = Document & Record<string, unknown>;
|
|
30
|
+
/** Generic model type */
|
|
31
|
+
type AnyModel = Model<AnyDocument>;
|
|
32
|
+
/** Sort direction */
|
|
33
|
+
type SortDirection = 1 | -1;
|
|
34
|
+
/** Sort specification for MongoDB queries */
|
|
35
|
+
type SortSpec = Record<string, SortDirection>;
|
|
36
|
+
/** Populate specification */
|
|
37
|
+
type PopulateSpec = string | string[] | PopulateOptions | PopulateOptions[];
|
|
38
|
+
/**
|
|
39
|
+
* Select specification. `readonly string[]` widens the legacy `string[]`
|
|
40
|
+
* form to also accept the immutable variant repo-core's contract uses
|
|
41
|
+
* (`LookupPopulateOptions.select: readonly string[] | Record<string, 0
|
|
42
|
+
* | 1>`), so `Repository<TDoc>` assigns to `StandardRepo<TDoc>` without
|
|
43
|
+
* TS2322 on the `select` field. Any existing mutable-array caller stays
|
|
44
|
+
* compatible — `string[]` is assignable to `readonly string[]`.
|
|
45
|
+
*/
|
|
46
|
+
type SelectSpec = string | readonly string[] | Record<string, 0 | 1>;
|
|
47
|
+
/** Filter query type for MongoDB queries (compatible with Mongoose 8 & 9) */
|
|
48
|
+
type FilterQuery<_T = unknown> = Record<string, unknown>;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/pagination/utils/cursor-signing.d.ts
|
|
51
|
+
/**
|
|
52
|
+
* HMAC signing for pagination cursors.
|
|
53
|
+
*
|
|
54
|
+
* ## What this protects, and what it does not
|
|
55
|
+
*
|
|
56
|
+
* A cursor is a POSITION, and the server's own filters decide what a caller may
|
|
57
|
+
* read — so a stolen or shared cursor cannot reach another tenant's rows, and
|
|
58
|
+
* signing is not a substitute for scoping. What an unsigned cursor gives away
|
|
59
|
+
* is the ability to MOVE the position: edit the encoded sort value, re-submit,
|
|
60
|
+
* and the response says whether any row sorts past it. Repeat, and that is a
|
|
61
|
+
* binary search over a column the caller can never read directly — "where do
|
|
62
|
+
* salaries above X start", against a list that only ever showed them names.
|
|
63
|
+
*
|
|
64
|
+
* Signing closes that. It does NOT hide the cursor's contents: the payload is
|
|
65
|
+
* still base64url JSON and still readable by anyone holding the token, field
|
|
66
|
+
* names included. **Signing is integrity, not confidentiality.** A deployment
|
|
67
|
+
* that must not reveal which column it sorts by needs encryption, which this is
|
|
68
|
+
* not.
|
|
69
|
+
*
|
|
70
|
+
* ## The rules that make it worth having
|
|
71
|
+
*
|
|
72
|
+
* - **A configured secret makes a signature REQUIRED.** Accepting an unsigned
|
|
73
|
+
* token "for compatibility" means an attacker strips the signature and the
|
|
74
|
+
* protection evaporates while still looking enabled — the exact shape of a
|
|
75
|
+
* specific instruction defeated by a general default.
|
|
76
|
+
* - **An unusable secret THROWS.** `cursorSecret: ''` (an unset env var read
|
|
77
|
+
* without a check) would otherwise mean "signing on, key guessable", and
|
|
78
|
+
* nothing would say so.
|
|
79
|
+
* - **Comparison is timing-safe.** `===` on a signature leaks it byte by byte
|
|
80
|
+
* to anyone willing to time the responses.
|
|
81
|
+
* - **Rotation takes a list.** Sign with the first, accept any — otherwise
|
|
82
|
+
* changing the key invalidates every cursor a client is holding.
|
|
83
|
+
*/
|
|
84
|
+
/** One key, or several during a rotation (the FIRST signs; all verify). */
|
|
85
|
+
type CursorSecret = string | readonly string[];
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/types/pagination.d.ts
|
|
88
|
+
/**
|
|
89
|
+
* How an offset page learns how many rows sit behind it.
|
|
90
|
+
*
|
|
91
|
+
* | | cost | `total` reports |
|
|
92
|
+
* |---|---|---|
|
|
93
|
+
* | `exact` | O(matching index keys) — the whole range, every page | the truth |
|
|
94
|
+
* | `estimated` | O(1) collection metadata, but IGNORES filters, so it falls back to `exact` whenever any filter is present | the truth, or the collection size |
|
|
95
|
+
* | `capped` | O(countLimit) — the scan stops at the ceiling | the truth below the ceiling, else the ceiling, with `totalIsLowerBound: true` |
|
|
96
|
+
* | `none` | free — `limit + 1` is fetched instead | `0` |
|
|
97
|
+
*
|
|
98
|
+
* `exact` is the default and is right until a filter matches enough rows for
|
|
99
|
+
* the count to outgrow the find it runs beside. Past that it is the dominant
|
|
100
|
+
* cost of a page the user reads fifteen rows of, and it is paid again on every
|
|
101
|
+
* page.
|
|
102
|
+
*
|
|
103
|
+
* `capped` is what a product ships instead: the row count a list header shows
|
|
104
|
+
* is only actionable while it is small, so bound the work and say `10,000+`
|
|
105
|
+
* above the bound. Elasticsearch (`track_total_hits`), Odoo (`search_count`'s
|
|
106
|
+
* limit) and GitHub's `1000+` are all this strategy.
|
|
107
|
+
*
|
|
108
|
+
* Declared ONCE and referenced everywhere — this union was hand-copied to eight
|
|
109
|
+
* declarations, which is how the aggregate variant silently lost a member.
|
|
110
|
+
*/
|
|
111
|
+
type CountStrategy = 'exact' | 'estimated' | 'capped' | 'none';
|
|
112
|
+
/**
|
|
113
|
+
* What mongokit adds to repo-core's offset envelope, via its reserved `TExtra`
|
|
114
|
+
* slot: `OffsetPaginationResult<TDoc, MongokitPageExtras>`.
|
|
115
|
+
*
|
|
116
|
+
* Both members are optional and absent on an ordinary page, so every existing
|
|
117
|
+
* consumer of `OffsetPaginationResult<TDoc>` stays assignable.
|
|
118
|
+
*
|
|
119
|
+
* A `type` and not an `interface`: repo-core constrains `TExtra` to
|
|
120
|
+
* `Record<string, unknown>`, and an interface has no implicit index signature.
|
|
121
|
+
*/
|
|
122
|
+
type MongokitPageExtras = {
|
|
123
|
+
/** Deep-page or clamped-limit advice. Human-readable; never branch on it. */
|
|
124
|
+
warning?: string;
|
|
125
|
+
/**
|
|
126
|
+
* `total` is a FLOOR, not a count — the scan stopped at `countLimit`. Render
|
|
127
|
+
* it as `10,000+`, and never compute a percentage or a last-page number from
|
|
128
|
+
* a total carrying this flag.
|
|
129
|
+
*
|
|
130
|
+
* A separate boolean rather than a sentinel `total` (`-1`, `null`): a total
|
|
131
|
+
* that lies in a field typed `number` is read as a number by every consumer
|
|
132
|
+
* that has not been taught otherwise, which is the failure this flag exists
|
|
133
|
+
* to make impossible to miss.
|
|
134
|
+
*/
|
|
135
|
+
totalIsLowerBound?: boolean;
|
|
136
|
+
};
|
|
137
|
+
/** Pagination configuration */
|
|
138
|
+
interface PaginationConfig {
|
|
139
|
+
/** Default number of documents per page (default: 10) */
|
|
140
|
+
defaultLimit?: number;
|
|
141
|
+
/** Maximum allowed limit (default: 100) */
|
|
142
|
+
maxLimit?: number;
|
|
143
|
+
/** Maximum allowed page number (default: 10000) */
|
|
144
|
+
maxPage?: number;
|
|
145
|
+
/** Page number that triggers performance warning (default: 100) */
|
|
146
|
+
deepPageThreshold?: number;
|
|
147
|
+
/** Cursor version for forward compatibility (default: 1) */
|
|
148
|
+
cursorVersion?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Minimum cursor version accepted. Bump alongside `cursorVersion` when a
|
|
151
|
+
* breaking format change ships so stale client cursors are rejected with a
|
|
152
|
+
* clear error instead of silently paginating from the wrong position.
|
|
153
|
+
* Default: 1 (accept any cursor <= cursorVersion).
|
|
154
|
+
*/
|
|
155
|
+
minCursorVersion?: number;
|
|
156
|
+
/**
|
|
157
|
+
* Allowlist of primary sort fields for keyset pagination. When set, any
|
|
158
|
+
* `getAll({ sort })` whose primary (non-_id) field is not in this list
|
|
159
|
+
* throws at validation time.
|
|
160
|
+
*
|
|
161
|
+
* Use this to lock down keyset pagination to fields that are structurally
|
|
162
|
+
* guaranteed non-null in your schema — keyset sort across null/non-null
|
|
163
|
+
* boundaries is lossy (MongoDB's `$lt/$gt` semantics leave a gap at the
|
|
164
|
+
* type boundary, so not every doc is reachable).
|
|
165
|
+
*
|
|
166
|
+
* `_id` is always allowed regardless of this list.
|
|
167
|
+
* Undefined (default) = no allowlist, any sort field accepted.
|
|
168
|
+
*/
|
|
169
|
+
strictKeysetSortFields?: string[];
|
|
170
|
+
/** Use estimatedDocumentCount for faster counts on large collections */
|
|
171
|
+
useEstimatedCount?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Repo-wide `countStrategy` for `getAll` / `lookupPopulate` /
|
|
174
|
+
* `aggregatePaginate` calls that don't set their own (default: 'exact' —
|
|
175
|
+
* a `countDocuments` round-trip per page). `'none'` suits list endpoints
|
|
176
|
+
* that only need `hasNext`: one query per page, `total` / `pages` report
|
|
177
|
+
* 0. A per-call `countStrategy` always wins.
|
|
178
|
+
*/
|
|
179
|
+
defaultCountStrategy?: CountStrategy;
|
|
180
|
+
/**
|
|
181
|
+
* Ceiling for `countStrategy: 'capped'` when a call names none. Default 10000.
|
|
182
|
+
* See {@link OffsetPaginationOptions.countLimit}.
|
|
183
|
+
*/
|
|
184
|
+
defaultCountLimit?: number;
|
|
185
|
+
/**
|
|
186
|
+
* HMAC key for cursor tokens. Absent (default) = cursors are unsigned, as
|
|
187
|
+
* before. A list rotates keys: the FIRST signs, all of them verify.
|
|
188
|
+
*
|
|
189
|
+
* Signing stops a caller EDITING a cursor to move the position — re-submit a
|
|
190
|
+
* tampered sort value and the response says whether any row sorts past it,
|
|
191
|
+
* which is a binary search over a column the caller may never read. It does
|
|
192
|
+
* NOT hide the token's contents: the payload stays readable base64url JSON.
|
|
193
|
+
* Integrity, not confidentiality.
|
|
194
|
+
*
|
|
195
|
+
* **Switching it on is a cutover.** Cursors already in flight carry no
|
|
196
|
+
* signature and will be refused — the client restarts from page one. That is
|
|
197
|
+
* deliberate: a "tolerate unsigned during rollout" flag is a permanent
|
|
198
|
+
* downgrade switch, and stripping the signature is the first thing an
|
|
199
|
+
* attacker tries.
|
|
200
|
+
*
|
|
201
|
+
* A key under 16 characters throws rather than being accepted, because an
|
|
202
|
+
* unset environment variable reads as `''` and would otherwise mean "signing
|
|
203
|
+
* on, key guessable" with nothing to say so.
|
|
204
|
+
*/
|
|
205
|
+
cursorSecret?: CursorSecret;
|
|
206
|
+
/**
|
|
207
|
+
* Repo-wide pagination mode for a `getAll` call that carries no explicit
|
|
208
|
+
* `mode`, no `page` and no cursor (`after`) — the plain
|
|
209
|
+
* `getAll({ filters })` case, which otherwise picks offset (or keyset
|
|
210
|
+
* only when a non-default sort is given). `'keyset'` makes those calls
|
|
211
|
+
* cursor-paginated: no `skip`, O(1) deep pages. Explicit per-call
|
|
212
|
+
* signals still win — `page` → offset, `after` → keyset, `mode` → as
|
|
213
|
+
* given. Default: undefined (the sort-based auto-detection above).
|
|
214
|
+
*/
|
|
215
|
+
defaultMode?: 'offset' | 'keyset';
|
|
216
|
+
}
|
|
217
|
+
/** MongoDB collation options for locale-aware string comparison */
|
|
218
|
+
interface CollationOptions {
|
|
219
|
+
locale: string;
|
|
220
|
+
caseLevel?: boolean;
|
|
221
|
+
caseFirst?: 'upper' | 'lower' | 'off';
|
|
222
|
+
strength?: 1 | 2 | 3 | 4 | 5;
|
|
223
|
+
numericOrdering?: boolean;
|
|
224
|
+
alternate?: 'non-ignorable' | 'shifted';
|
|
225
|
+
maxVariable?: 'punct' | 'space';
|
|
226
|
+
backwards?: boolean;
|
|
227
|
+
}
|
|
228
|
+
/** Base pagination options */
|
|
229
|
+
interface BasePaginationOptions {
|
|
230
|
+
/** Pagination mode (explicit override) */
|
|
231
|
+
mode?: 'offset' | 'keyset';
|
|
232
|
+
/** MongoDB query filters */
|
|
233
|
+
filters?: FilterQuery<AnyDocument>;
|
|
234
|
+
/** Sort specification */
|
|
235
|
+
sort?: SortSpec;
|
|
236
|
+
/** Number of documents per page */
|
|
237
|
+
limit?: number;
|
|
238
|
+
/** Fields to select */
|
|
239
|
+
select?: SelectSpec;
|
|
240
|
+
/** Fields to populate */
|
|
241
|
+
populate?: PopulateSpec;
|
|
242
|
+
/** Return plain JavaScript objects */
|
|
243
|
+
lean?: boolean;
|
|
244
|
+
/** MongoDB session for transactions */
|
|
245
|
+
session?: unknown;
|
|
246
|
+
/** Query hint (index name or document) */
|
|
247
|
+
hint?: string | Record<string, 1 | -1>;
|
|
248
|
+
/** Maximum execution time in milliseconds */
|
|
249
|
+
maxTimeMS?: number;
|
|
250
|
+
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
251
|
+
readPreference?: ReadPreferenceType;
|
|
252
|
+
/** Collation for locale-aware string comparison and case-insensitive sorting */
|
|
253
|
+
collation?: CollationOptions;
|
|
254
|
+
}
|
|
255
|
+
/** Offset pagination options */
|
|
256
|
+
interface OffsetPaginationOptions extends BasePaginationOptions {
|
|
257
|
+
/** Page number (1-indexed) */
|
|
258
|
+
page?: number;
|
|
259
|
+
/** Count strategy for filtered queries (default: 'exact'). See {@link CountStrategy}. */
|
|
260
|
+
countStrategy?: CountStrategy;
|
|
261
|
+
/**
|
|
262
|
+
* Ceiling for `countStrategy: 'capped'` (default: the repository's
|
|
263
|
+
* `defaultCountLimit`, else 10000). Ignored by every other strategy.
|
|
264
|
+
*
|
|
265
|
+
* A `total` that comes back EQUAL to this is a lower bound, not a count —
|
|
266
|
+
* `totalIsLowerBound` on the result says so, because `total === countLimit`
|
|
267
|
+
* is also what an exactly-full collection legitimately returns and the two
|
|
268
|
+
* are otherwise indistinguishable.
|
|
269
|
+
*/
|
|
270
|
+
countLimit?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Optional alternate filter used for the count query only. The primary
|
|
273
|
+
* `filters` is still used for `.find()`. Lets callers (Repository) run a
|
|
274
|
+
* sort-only query like `$near` for results while counting with a
|
|
275
|
+
* count-compatible rewrite (e.g. `$geoWithin: $centerSphere`) that
|
|
276
|
+
* returns the same document set but works with `countDocuments`.
|
|
277
|
+
*/
|
|
278
|
+
countFilters?: FilterQuery<AnyDocument>;
|
|
279
|
+
}
|
|
280
|
+
/** Keyset (cursor) pagination options */
|
|
281
|
+
interface KeysetPaginationOptions extends BasePaginationOptions {
|
|
282
|
+
/** Cursor token for next page */
|
|
283
|
+
after?: string;
|
|
284
|
+
/**
|
|
285
|
+
* Cursor token for the PREVIOUS page — the page ending just before this
|
|
286
|
+
* position. Mutually exclusive with `after`; passing both throws.
|
|
287
|
+
*
|
|
288
|
+
* Feed back the `prev` from a result. Rows come back in the sort order you
|
|
289
|
+
* asked for, not reversed: the query runs inverted internally and the page
|
|
290
|
+
* is turned back around before it is returned.
|
|
291
|
+
*/
|
|
292
|
+
before?: string;
|
|
293
|
+
/** Sort is required for keyset pagination */
|
|
294
|
+
sort: SortSpec;
|
|
295
|
+
}
|
|
296
|
+
/** Aggregate pagination options */
|
|
297
|
+
interface AggregatePaginationOptions {
|
|
298
|
+
/** Aggregation pipeline stages */
|
|
299
|
+
pipeline?: PipelineStage[];
|
|
300
|
+
/** Page number (1-indexed) */
|
|
301
|
+
page?: number;
|
|
302
|
+
/** Number of documents per page */
|
|
303
|
+
limit?: number;
|
|
304
|
+
/** MongoDB session for transactions */
|
|
305
|
+
session?: unknown;
|
|
306
|
+
/** Query hint (index name or document) for aggregation */
|
|
307
|
+
hint?: string | Record<string, 1 | -1>;
|
|
308
|
+
/** Maximum execution time in milliseconds */
|
|
309
|
+
maxTimeMS?: number;
|
|
310
|
+
/** Count strategy (default: 'exact' via $facet).
|
|
311
|
+
* 'estimated' is accepted but treated as 'exact' in aggregation context
|
|
312
|
+
* since estimatedDocumentCount is not available inside pipelines.
|
|
313
|
+
* 'capped' bounds the count facet with a `$limit` — see {@link CountStrategy}. */
|
|
314
|
+
countStrategy?: CountStrategy;
|
|
315
|
+
/** Ceiling for `countStrategy: 'capped'`. See {@link OffsetPaginationOptions.countLimit}. */
|
|
316
|
+
countLimit?: number;
|
|
317
|
+
/**
|
|
318
|
+
* Let `$sort` / `$group` spill to disk past the server's 100MB per-stage
|
|
319
|
+
* memory limit. Off by default — spilling hides a missing index behind a
|
|
320
|
+
* slow success, and on MongoDB >= 6.0 the server's own
|
|
321
|
+
* `allowDiskUseByDefault` already decides this when a caller says nothing.
|
|
322
|
+
*/
|
|
323
|
+
allowDiskUse?: boolean;
|
|
324
|
+
/** Pagination mode (reserved for API consistency) */
|
|
325
|
+
mode?: 'offset';
|
|
326
|
+
/** Read preference for replica sets (e.g. 'secondaryPreferred') */
|
|
327
|
+
readPreference?: ReadPreferenceType;
|
|
328
|
+
}
|
|
329
|
+
/** Value type identifier for cursor serialization */
|
|
330
|
+
type ValueType = 'date' | 'objectid' | 'boolean' | 'number' | 'string' | 'null' | 'unknown';
|
|
331
|
+
/** Cursor payload */
|
|
332
|
+
interface CursorPayload {
|
|
333
|
+
/** Primary sort field value (legacy single-field) */
|
|
334
|
+
v: string | number | boolean | null;
|
|
335
|
+
/** Value type identifier (legacy single-field) */
|
|
336
|
+
t: ValueType;
|
|
337
|
+
/** Document ID */
|
|
338
|
+
id: string;
|
|
339
|
+
/** ID type identifier */
|
|
340
|
+
idType: ValueType;
|
|
341
|
+
/** Sort specification */
|
|
342
|
+
sort: SortSpec;
|
|
343
|
+
/** Cursor version */
|
|
344
|
+
ver: number;
|
|
345
|
+
/** Compound sort field values (multi-field keyset) */
|
|
346
|
+
vals?: Record<string, string | number | boolean | null>;
|
|
347
|
+
/** Compound sort value types */
|
|
348
|
+
types?: Record<string, ValueType>;
|
|
349
|
+
}
|
|
350
|
+
/** Decoded cursor */
|
|
351
|
+
interface DecodedCursor {
|
|
352
|
+
/** Primary sort field value (rehydrated) — legacy compat */
|
|
353
|
+
value: unknown;
|
|
354
|
+
/** Document ID (rehydrated) */
|
|
355
|
+
id: ObjectId | string;
|
|
356
|
+
/** Sort specification */
|
|
357
|
+
sort: SortSpec;
|
|
358
|
+
/** Cursor version */
|
|
359
|
+
version: number;
|
|
360
|
+
/** All sort field values (rehydrated) — for compound sort */
|
|
361
|
+
values?: Record<string, unknown>;
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
export { PopulateSpec as _, CursorPayload as a, SortDirection as b, MongokitPageExtras as c, ValueType as d, CursorSecret as f, ObjectId as g, FilterQuery as h, CountStrategy as i, OffsetPaginationOptions as l, AnyModel as m, BasePaginationOptions as n, DecodedCursor as o, AnyDocument as p, CollationOptions as r, KeysetPaginationOptions as s, AggregatePaginationOptions as t, PaginationConfig as u, ReadPreferenceType as v, SortSpec as x, SelectSpec as y };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { h as RepositoryInstance, p as RepositoryContext } from "./repository-
|
|
1
|
+
import { _ as PopulateSpec, g as ObjectId, x as SortSpec, y as SelectSpec } from "./pagination-BpIv2bdS.mjs";
|
|
2
|
+
import { h as RepositoryInstance, p as RepositoryContext } from "./repository-ZbLtL7Nk.mjs";
|
|
3
3
|
import { OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
4
4
|
//#region src/types/plugin-options.d.ts
|
|
5
5
|
/** Field preset configuration (field-filter plugin) */
|