@classytic/mongokit 3.36.0 → 3.38.1
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-GdyX29tE.mjs → PaginationEngine-BX9o_-4R.mjs} +2 -64
- package/dist/{Repository-BArvU32x.mjs → Repository-Ck5x43wF.mjs} +9 -4
- package/dist/better-auth/index.d.mts +78 -1
- package/dist/better-auth/index.mjs +232 -2
- package/dist/{create-repository-CPNbktFG.mjs → create-repository-B5EulQ31.mjs} +2 -2
- package/dist/index.mjs +28 -5
- package/dist/indexes/verify-indexes.d.mts +69 -0
- package/dist/indexes/verify-indexes.mjs +76 -0
- package/dist/outbox/index.mjs +1 -1
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/pagination/utils/limits.d.mts +47 -0
- package/dist/pagination/utils/limits.mjs +64 -0
- package/dist/plugins/index.mjs +2 -2
- package/dist/testkit/index.mjs +1 -1
- package/dist/{timestamp.plugin-BEc6TANm.mjs → timestamp.plugin-CBQukQzV.mjs} +45 -1
- package/dist/{validation-chain.plugin-CyK1s9Ze.mjs → validation-chain.plugin-DhNyxNCw.mjs} +1 -1
- package/package.json +11 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
3
3
|
import { t as createError } from "./error-BuByZwKU.mjs";
|
|
4
|
+
import { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage } from "./pagination/utils/limits.mjs";
|
|
4
5
|
import mongoose from "mongoose";
|
|
5
6
|
//#region src/pagination/utils/filter.ts
|
|
6
7
|
/**
|
|
@@ -374,69 +375,6 @@ function hasCompatibleKeysetIndex(indexes, filterFields, sort) {
|
|
|
374
375
|
return false;
|
|
375
376
|
}
|
|
376
377
|
//#endregion
|
|
377
|
-
//#region src/pagination/utils/limits.ts
|
|
378
|
-
/**
|
|
379
|
-
* Validates and sanitizes limit value
|
|
380
|
-
* Parses strings to numbers and prevents NaN bugs
|
|
381
|
-
*
|
|
382
|
-
* @param limit - Requested limit
|
|
383
|
-
* @param config - Pagination configuration
|
|
384
|
-
* @returns Sanitized limit between 1 and maxLimit
|
|
385
|
-
*/
|
|
386
|
-
function validateLimit(limit, config) {
|
|
387
|
-
const parsed = Number(limit);
|
|
388
|
-
if (!Number.isFinite(parsed) || parsed < 1) return config.defaultLimit ?? 10;
|
|
389
|
-
const max = config.maxLimit ?? 100;
|
|
390
|
-
if (max === 0) return Math.floor(parsed);
|
|
391
|
-
return Math.min(Math.floor(parsed), max);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Validates and sanitizes page number
|
|
395
|
-
* Parses strings to numbers and prevents NaN bugs
|
|
396
|
-
*
|
|
397
|
-
* @param page - Requested page (1-indexed)
|
|
398
|
-
* @param config - Pagination configuration
|
|
399
|
-
* @returns Sanitized page number >= 1
|
|
400
|
-
* @throws Error if page exceeds maxPage
|
|
401
|
-
*/
|
|
402
|
-
function validatePage(page, config) {
|
|
403
|
-
const parsed = Number(page);
|
|
404
|
-
if (!Number.isFinite(parsed) || parsed < 1) return 1;
|
|
405
|
-
const sanitized = Math.floor(parsed);
|
|
406
|
-
if (sanitized > (config.maxPage || 1e4)) throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
|
|
407
|
-
return sanitized;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Checks if page number should trigger deep pagination warning
|
|
411
|
-
*
|
|
412
|
-
* @param page - Current page number
|
|
413
|
-
* @param threshold - Warning threshold
|
|
414
|
-
* @returns True if warning should be shown
|
|
415
|
-
*/
|
|
416
|
-
function shouldWarnDeepPagination(page, threshold) {
|
|
417
|
-
return page > threshold;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Calculates number of documents to skip for offset pagination
|
|
421
|
-
*
|
|
422
|
-
* @param page - Page number (1-indexed)
|
|
423
|
-
* @param limit - Documents per page
|
|
424
|
-
* @returns Number of documents to skip
|
|
425
|
-
*/
|
|
426
|
-
function calculateSkip(page, limit) {
|
|
427
|
-
return (page - 1) * limit;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Calculates total number of pages
|
|
431
|
-
*
|
|
432
|
-
* @param total - Total document count
|
|
433
|
-
* @param limit - Documents per page
|
|
434
|
-
* @returns Total number of pages
|
|
435
|
-
*/
|
|
436
|
-
function calculateTotalPages(total, limit) {
|
|
437
|
-
return Math.ceil(total / limit);
|
|
438
|
-
}
|
|
439
|
-
//#endregion
|
|
440
378
|
//#region src/pagination/utils/sort.ts
|
|
441
379
|
var sort_exports = /* @__PURE__ */ __exportAll({
|
|
442
380
|
getPrimaryField: () => getPrimaryField,
|
|
@@ -794,4 +732,4 @@ var PaginationEngine = class {
|
|
|
794
732
|
}
|
|
795
733
|
};
|
|
796
734
|
//#endregion
|
|
797
|
-
export {
|
|
735
|
+
export { sort_exports as n, cursor_exports as r, PaginationEngine as t };
|
|
@@ -3,7 +3,8 @@ import { a as narrowToIds, i as keysetFilter, o as selectKeysetChunk } from "./i
|
|
|
3
3
|
import { a as count, b as LookupBuilder, c as getByQuery, d as deleteById, f as deleteByQuery, h as createMany, l as getOrCreate, m as create, n as update, o as exists, r as updateByQuery, s as getById, t as findOneAndUpdate, x as compileFilterToMongo, y as distinct } from "./update-jSXp3nu3.mjs";
|
|
4
4
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { t as PaginationEngine } from "./PaginationEngine-BX9o_-4R.mjs";
|
|
7
|
+
import { calculateTotalPages } from "./pagination/utils/limits.mjs";
|
|
7
8
|
import { hasNearOperator, rewriteNearForCount } from "./query/primitives/geo.mjs";
|
|
8
9
|
import { RepositoryBase, runChunkedArchive, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry } from "@classytic/repo-core/repository";
|
|
9
10
|
import mongoose from "mongoose";
|
|
@@ -2635,6 +2636,10 @@ var Repository = class extends RepositoryBase {
|
|
|
2635
2636
|
const idType = getSchemaIdType(this.Model.schema);
|
|
2636
2637
|
if (!isValidIdForType(id, idType)) {
|
|
2637
2638
|
if (wantsThrow) throw createError(404, "Document not found");
|
|
2639
|
+
await this._emitHook("after:getById", {
|
|
2640
|
+
context,
|
|
2641
|
+
result: null
|
|
2642
|
+
});
|
|
2638
2643
|
return null;
|
|
2639
2644
|
}
|
|
2640
2645
|
const result = await this._withResilience(context, () => getById(this.Model, id, context));
|
|
@@ -4412,9 +4417,9 @@ var Repository = class extends RepositoryBase {
|
|
|
4412
4417
|
const appendLookupStages$2 = (pipeline) => appendLookupStages(pipeline, lookups, projection, isKeyset && sort ? this._parseSort(sort) : void 0);
|
|
4413
4418
|
if (isKeyset && sort) {
|
|
4414
4419
|
const parsedSort = this._parseSort(sort);
|
|
4415
|
-
const { validateKeysetSort } = await import("./PaginationEngine-
|
|
4416
|
-
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-
|
|
4417
|
-
const { getPrimaryField } = await import("./PaginationEngine-
|
|
4420
|
+
const { validateKeysetSort } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.n);
|
|
4421
|
+
const { encodeCursor, resolveCursorFilter } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.r);
|
|
4422
|
+
const { getPrimaryField } = await import("./PaginationEngine-BX9o_-4R.mjs").then((n) => n.n);
|
|
4418
4423
|
const normalizedSort = validateKeysetSort(parsedSort, this._pagination.config.strictKeysetSortFields);
|
|
4419
4424
|
const cursorVersion = this._pagination.config.cursorVersion ?? 1;
|
|
4420
4425
|
const minCursorVersion = this._pagination.config.minCursorVersion ?? 1;
|
|
@@ -259,6 +259,83 @@ declare function ensureBetterAuthSessionTtl(connection: {
|
|
|
259
259
|
collection?: string;
|
|
260
260
|
indexName?: string;
|
|
261
261
|
}): Promise<boolean>;
|
|
262
|
+
/**
|
|
263
|
+
* One declared index on a Better Auth collection.
|
|
264
|
+
*
|
|
265
|
+
* `unique` is a REQUEST, not a guarantee: {@link ensureBetterAuthIndexes}
|
|
266
|
+
* downgrades to a non-unique index when existing rows already violate it,
|
|
267
|
+
* because failing a boot over historical duplicates is worse than running with
|
|
268
|
+
* a plain lookup index and reporting the degradation.
|
|
269
|
+
*/
|
|
270
|
+
interface BetterAuthIndexSpec {
|
|
271
|
+
/** Canonical collection name (`'user'`, `'session'`, ...). */
|
|
272
|
+
collection: string;
|
|
273
|
+
keys: Record<string, 1 | -1>;
|
|
274
|
+
name: string;
|
|
275
|
+
unique?: boolean;
|
|
276
|
+
/** Present on self-expiring rows; always paired with `expireAfterSeconds: 0`. */
|
|
277
|
+
ttl?: boolean;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The indexes Better Auth's own queries need, which its mongo adapter does NOT
|
|
281
|
+
* create. Grouped by the plugin that owns the collection so a host only pays
|
|
282
|
+
* for what it enabled.
|
|
283
|
+
*
|
|
284
|
+
* Every entry below backs a query BA runs on a hot path:
|
|
285
|
+
* - `session.token` — read on EVERY authenticated request
|
|
286
|
+
* - `user.email` — read on every sign-in / sign-up, and must be unique
|
|
287
|
+
* - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
|
|
288
|
+
* - `member.(organizationId, userId)` — the membership check on org-scoped requests
|
|
289
|
+
* Without them each of those is a collection scan, and `email` / `slug` /
|
|
290
|
+
* `token` have no uniqueness guarantee at all.
|
|
291
|
+
*
|
|
292
|
+
* TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
|
|
293
|
+
* {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
|
|
294
|
+
*/
|
|
295
|
+
declare const BA_INDEXES_BY_PLUGIN: Record<string, readonly BetterAuthIndexSpec[]>;
|
|
296
|
+
/** Outcome of one declared index. */
|
|
297
|
+
interface BetterAuthIndexResult {
|
|
298
|
+
collection: string;
|
|
299
|
+
name: string;
|
|
300
|
+
status: 'created' | 'degraded' | 'failed';
|
|
301
|
+
/** Set on `degraded` (unique dropped) and `failed`. */
|
|
302
|
+
reason?: string;
|
|
303
|
+
}
|
|
304
|
+
interface EnsureBetterAuthIndexesOptions {
|
|
305
|
+
/** Plugin sets to include beyond `core` (always implied). */
|
|
306
|
+
plugins?: BetterAuthPluginKey$1[];
|
|
307
|
+
/** Mirror BA's `usePlural` — appends `s` to every collection name. */
|
|
308
|
+
usePlural?: boolean;
|
|
309
|
+
/** Per-collection name override (mirrors BA's `user.modelName`). */
|
|
310
|
+
modelOverrides?: Partial<Record<string, string>>;
|
|
311
|
+
/** Canonical collection names to skip entirely. */
|
|
312
|
+
exclude?: string[];
|
|
313
|
+
}
|
|
314
|
+
interface IndexCreator {
|
|
315
|
+
collection(name: string): {
|
|
316
|
+
createIndex(keys: Record<string, 1 | -1>, opts: Record<string, unknown>): Promise<unknown>;
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Create every index Better Auth's queries need. Idempotent, never throws.
|
|
321
|
+
*
|
|
322
|
+
* BA's mongo adapter creates NO indexes — not even on `session.token`, which is
|
|
323
|
+
* read on every authenticated request, nor a unique one on `user.email`. On a
|
|
324
|
+
* small database nobody notices; the cost arrives as a full collection scan per
|
|
325
|
+
* login once the tables grow, and until then nothing stops a duplicate email.
|
|
326
|
+
*
|
|
327
|
+
* This lives in the kit rather than a host for the same reason
|
|
328
|
+
* {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
|
|
329
|
+
* names, so fixing it here protects every consumer instead of one host.
|
|
330
|
+
*
|
|
331
|
+
* **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
|
|
332
|
+
* without uniqueness when the collection already holds violating rows, and
|
|
333
|
+
* reported as `degraded` — a boot must not die because historical data has a
|
|
334
|
+
* duplicate. Inspect the returned report to find those.
|
|
335
|
+
*
|
|
336
|
+
* Call once at boot, after the connection is live. Safe to call again.
|
|
337
|
+
*/
|
|
338
|
+
declare function ensureBetterAuthIndexes(connection: IndexCreator, options?: EnsureBetterAuthIndexesOptions): Promise<BetterAuthIndexResult[]>;
|
|
262
339
|
interface SessionUpdaterLike {
|
|
263
340
|
updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
|
|
264
341
|
modifiedCount?: number;
|
|
@@ -293,4 +370,4 @@ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLik
|
|
|
293
370
|
userId?: unknown;
|
|
294
371
|
}): Promise<number>;
|
|
295
372
|
//#endregion
|
|
296
|
-
export { BetterAuthIdType, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
|
373
|
+
export { BA_INDEXES_BY_PLUGIN, BetterAuthIdType, BetterAuthIndexResult, BetterAuthIndexSpec, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, EnsureBetterAuthIndexesOptions, IndexCreator, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-Ck5x43wF.mjs";
|
|
2
2
|
import { n as createMongooseAdapter } from "../adapter-Bfqz8yHE.mjs";
|
|
3
3
|
import { asReadOnlyRepo } from "@classytic/repo-core/repository";
|
|
4
4
|
import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
|
|
@@ -203,6 +203,236 @@ async function ensureBetterAuthSessionTtl(connection, options = {}) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
|
+
* The indexes Better Auth's own queries need, which its mongo adapter does NOT
|
|
207
|
+
* create. Grouped by the plugin that owns the collection so a host only pays
|
|
208
|
+
* for what it enabled.
|
|
209
|
+
*
|
|
210
|
+
* Every entry below backs a query BA runs on a hot path:
|
|
211
|
+
* - `session.token` — read on EVERY authenticated request
|
|
212
|
+
* - `user.email` — read on every sign-in / sign-up, and must be unique
|
|
213
|
+
* - `account.(providerId|issuer, accountId)` — the credential lookup at sign-in
|
|
214
|
+
* - `member.(organizationId, userId)` — the membership check on org-scoped requests
|
|
215
|
+
* Without them each of those is a collection scan, and `email` / `slug` /
|
|
216
|
+
* `token` have no uniqueness guarantee at all.
|
|
217
|
+
*
|
|
218
|
+
* TTL entries (`expiresAt`) additionally keep the collection BOUNDED — see
|
|
219
|
+
* {@link ensureBetterAuthSessionTtl} for the measurements that motivated it.
|
|
220
|
+
*/
|
|
221
|
+
const BA_INDEXES_BY_PLUGIN = {
|
|
222
|
+
core: [
|
|
223
|
+
{
|
|
224
|
+
collection: "user",
|
|
225
|
+
keys: { email: 1 },
|
|
226
|
+
name: "ba_user_email",
|
|
227
|
+
unique: true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
collection: "session",
|
|
231
|
+
keys: { token: 1 },
|
|
232
|
+
name: "ba_session_token",
|
|
233
|
+
unique: true
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
collection: "session",
|
|
237
|
+
keys: { userId: 1 },
|
|
238
|
+
name: "ba_session_userId"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
collection: "session",
|
|
242
|
+
keys: { expiresAt: 1 },
|
|
243
|
+
name: "ttl_session_expiresAt",
|
|
244
|
+
ttl: true
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
collection: "account",
|
|
248
|
+
keys: { userId: 1 },
|
|
249
|
+
name: "ba_account_userId"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
collection: "account",
|
|
253
|
+
keys: {
|
|
254
|
+
providerId: 1,
|
|
255
|
+
accountId: 1
|
|
256
|
+
},
|
|
257
|
+
name: "ba_account_provider_accountId"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
collection: "account",
|
|
261
|
+
keys: {
|
|
262
|
+
issuer: 1,
|
|
263
|
+
accountId: 1
|
|
264
|
+
},
|
|
265
|
+
name: "ba_account_issuer_accountId"
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
collection: "verification",
|
|
269
|
+
keys: { identifier: 1 },
|
|
270
|
+
name: "ba_verification_identifier"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
collection: "verification",
|
|
274
|
+
keys: { expiresAt: 1 },
|
|
275
|
+
name: "ttl_verification_expiresAt",
|
|
276
|
+
ttl: true
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
organization: [
|
|
280
|
+
{
|
|
281
|
+
collection: "organization",
|
|
282
|
+
keys: { slug: 1 },
|
|
283
|
+
name: "ba_organization_slug",
|
|
284
|
+
unique: true
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
collection: "member",
|
|
288
|
+
keys: {
|
|
289
|
+
organizationId: 1,
|
|
290
|
+
userId: 1
|
|
291
|
+
},
|
|
292
|
+
name: "ba_member_org_user",
|
|
293
|
+
unique: true
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
collection: "member",
|
|
297
|
+
keys: { userId: 1 },
|
|
298
|
+
name: "ba_member_userId"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
collection: "invitation",
|
|
302
|
+
keys: { organizationId: 1 },
|
|
303
|
+
name: "ba_invitation_org"
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
collection: "invitation",
|
|
307
|
+
keys: { email: 1 },
|
|
308
|
+
name: "ba_invitation_email"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
collection: "invitation",
|
|
312
|
+
keys: { expiresAt: 1 },
|
|
313
|
+
name: "ttl_invitation_expiresAt",
|
|
314
|
+
ttl: true
|
|
315
|
+
}
|
|
316
|
+
],
|
|
317
|
+
"organization-teams": [{
|
|
318
|
+
collection: "team",
|
|
319
|
+
keys: { organizationId: 1 },
|
|
320
|
+
name: "ba_team_org"
|
|
321
|
+
}, {
|
|
322
|
+
collection: "teamMember",
|
|
323
|
+
keys: {
|
|
324
|
+
teamId: 1,
|
|
325
|
+
userId: 1
|
|
326
|
+
},
|
|
327
|
+
name: "ba_teamMember_team_user"
|
|
328
|
+
}],
|
|
329
|
+
twoFactor: [{
|
|
330
|
+
collection: "twoFactor",
|
|
331
|
+
keys: { userId: 1 },
|
|
332
|
+
name: "ba_twoFactor_userId"
|
|
333
|
+
}],
|
|
334
|
+
deviceAuthorization: [
|
|
335
|
+
{
|
|
336
|
+
collection: "deviceCode",
|
|
337
|
+
keys: { deviceCode: 1 },
|
|
338
|
+
name: "ba_deviceCode_code"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
collection: "deviceCode",
|
|
342
|
+
keys: { userCode: 1 },
|
|
343
|
+
name: "ba_deviceCode_userCode"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
collection: "deviceCode",
|
|
347
|
+
keys: { expiresAt: 1 },
|
|
348
|
+
name: "ttl_deviceCode_expiresAt",
|
|
349
|
+
ttl: true
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Create every index Better Auth's queries need. Idempotent, never throws.
|
|
355
|
+
*
|
|
356
|
+
* BA's mongo adapter creates NO indexes — not even on `session.token`, which is
|
|
357
|
+
* read on every authenticated request, nor a unique one on `user.email`. On a
|
|
358
|
+
* small database nobody notices; the cost arrives as a full collection scan per
|
|
359
|
+
* login once the tables grow, and until then nothing stops a duplicate email.
|
|
360
|
+
*
|
|
361
|
+
* This lives in the kit rather than a host for the same reason
|
|
362
|
+
* {@link ensureBetterAuthSessionTtl} does: the kit is what knows BA's collection
|
|
363
|
+
* names, so fixing it here protects every consumer instead of one host.
|
|
364
|
+
*
|
|
365
|
+
* **Uniqueness degrades rather than fails.** A `unique: true` spec is retried
|
|
366
|
+
* without uniqueness when the collection already holds violating rows, and
|
|
367
|
+
* reported as `degraded` — a boot must not die because historical data has a
|
|
368
|
+
* duplicate. Inspect the returned report to find those.
|
|
369
|
+
*
|
|
370
|
+
* Call once at boot, after the connection is live. Safe to call again.
|
|
371
|
+
*/
|
|
372
|
+
async function ensureBetterAuthIndexes(connection, options = {}) {
|
|
373
|
+
const plugins = options.plugins ?? [];
|
|
374
|
+
const excluded = new Set(options.exclude ?? []);
|
|
375
|
+
const keys = ["core", ...plugins];
|
|
376
|
+
const resolveName = (canonical) => {
|
|
377
|
+
const overridden = options.modelOverrides?.[canonical] ?? canonical;
|
|
378
|
+
return options.usePlural ? pluralizeBetterAuthCollection(overridden) : overridden;
|
|
379
|
+
};
|
|
380
|
+
const specs = [];
|
|
381
|
+
const seen = /* @__PURE__ */ new Set();
|
|
382
|
+
for (const key of keys) for (const spec of BA_INDEXES_BY_PLUGIN[key] ?? []) {
|
|
383
|
+
if (excluded.has(spec.collection)) continue;
|
|
384
|
+
const id = `${spec.collection}.${spec.name}`;
|
|
385
|
+
if (seen.has(id)) continue;
|
|
386
|
+
seen.add(id);
|
|
387
|
+
specs.push(spec);
|
|
388
|
+
}
|
|
389
|
+
const results = [];
|
|
390
|
+
for (const spec of specs) {
|
|
391
|
+
const target = resolveName(spec.collection);
|
|
392
|
+
const base = { name: spec.name };
|
|
393
|
+
if (spec.ttl) base.expireAfterSeconds = 0;
|
|
394
|
+
try {
|
|
395
|
+
await connection.collection(target).createIndex(spec.keys, {
|
|
396
|
+
...base,
|
|
397
|
+
...spec.unique ? { unique: true } : {}
|
|
398
|
+
});
|
|
399
|
+
results.push({
|
|
400
|
+
collection: target,
|
|
401
|
+
name: spec.name,
|
|
402
|
+
status: "created"
|
|
403
|
+
});
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
406
|
+
if (!spec.unique) {
|
|
407
|
+
results.push({
|
|
408
|
+
collection: target,
|
|
409
|
+
name: spec.name,
|
|
410
|
+
status: "failed",
|
|
411
|
+
reason
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
await connection.collection(target).createIndex(spec.keys, base);
|
|
417
|
+
results.push({
|
|
418
|
+
collection: target,
|
|
419
|
+
name: spec.name,
|
|
420
|
+
status: "degraded",
|
|
421
|
+
reason
|
|
422
|
+
});
|
|
423
|
+
} catch (err2) {
|
|
424
|
+
results.push({
|
|
425
|
+
collection: target,
|
|
426
|
+
name: spec.name,
|
|
427
|
+
status: "failed",
|
|
428
|
+
reason: err2 instanceof Error ? err2.message : String(err2)
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return results;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
206
436
|
* Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
|
|
207
437
|
*
|
|
208
438
|
* WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
|
|
@@ -233,4 +463,4 @@ async function clearActiveOrganizationFromSessions(sessions, organizationId, opt
|
|
|
233
463
|
return (await sessions.updateMany(filter, { $set: { activeOrganizationId: null } })).modifiedCount ?? 0;
|
|
234
464
|
}
|
|
235
465
|
//#endregion
|
|
236
|
-
export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
|
466
|
+
export { BA_INDEXES_BY_PLUGIN, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthIndexes, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Repository } from "./Repository-
|
|
2
|
-
import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-
|
|
1
|
+
import { n as Repository } from "./Repository-Ck5x43wF.mjs";
|
|
2
|
+
import { a as customIdPlugin, f as auditTrailPlugin, i as multiTenantPlugin, m as auditLogPlugin, n as softDeletePlugin, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-CBQukQzV.mjs";
|
|
3
3
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
|
|
4
4
|
//#region src/create-repository.ts
|
|
5
5
|
function isLoggerAudit(value) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { n as idVariants } from "./id-resolution-BqDrk0S2.mjs";
|
|
2
|
-
import { _ as resetCapabilityProbeCache, a as supportsTransactions, b as transactionResolutionOf, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, g as probeMongoCapabilities, h as classifyHelloReply, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as capabilitiesForSupport, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY, v as resolveMongoCapabilities, x as createMongoPurgePort, y as resolveTransactionSupport } from "./Repository-
|
|
2
|
+
import { _ as resetCapabilityProbeCache, a as supportsTransactions, b as transactionResolutionOf, c as ALL_OPERATIONS, d as READ_OPERATIONS, f as operationsByPolicyKey, g as probeMongoCapabilities, h as classifyHelloReply, i as isTransactionUnsupported, l as MUTATING_OPERATIONS, m as capabilitiesForSupport, n as Repository, o as withTransaction, p as MONGOKIT_CAPABILITIES, r as batchTransaction, s as AggregationBuilder, t as HOOK_PRIORITY, u as OP_REGISTRY, v as resolveMongoCapabilities, x as createMongoPurgePort, y as resolveTransactionSupport } from "./Repository-Ck5x43wF.mjs";
|
|
3
3
|
import { b as LookupBuilder, x as compileFilterToMongo } from "./update-jSXp3nu3.mjs";
|
|
4
4
|
import { n as warn, t as configureLogger } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { n as isDuplicateKeyError, r as parseDuplicateKeyError, t as createError } from "./error-BuByZwKU.mjs";
|
|
6
6
|
import { t as actions_exports } from "./actions/index.mjs";
|
|
7
|
-
import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, r as adminBypass, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-
|
|
7
|
+
import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, r as adminBypass, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "./timestamp.plugin-CBQukQzV.mjs";
|
|
8
8
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "./method-registry.plugin-DblM7V6j.mjs";
|
|
9
|
-
import { t as PaginationEngine } from "./PaginationEngine-
|
|
9
|
+
import { t as PaginationEngine } from "./PaginationEngine-BX9o_-4R.mjs";
|
|
10
10
|
import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
11
|
-
import { t as createRepository } from "./create-repository-
|
|
11
|
+
import { t as createRepository } from "./create-repository-B5EulQ31.mjs";
|
|
12
12
|
import { n as defineModel, r as isModelRegistered, t as ModelCollisionError } from "./model-registry-ClKtWiHa.mjs";
|
|
13
|
-
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-
|
|
13
|
+
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "./validation-chain.plugin-DhNyxNCw.mjs";
|
|
14
14
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-FSQFLpsU.mjs";
|
|
15
15
|
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
16
16
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
@@ -509,6 +509,11 @@ function parseFilters(rt, filters, depth = 0) {
|
|
|
509
509
|
rt.reject(`Blocked filter field not in allowlist: ${baseField}`, { field: baseField });
|
|
510
510
|
continue;
|
|
511
511
|
}
|
|
512
|
+
if (key === "$and") {
|
|
513
|
+
const branches = parseCompoundBranches(rt, value, depth);
|
|
514
|
+
if (branches.length > 0) parsedFilters.$and = branches;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
512
517
|
if (operatorMatch) {
|
|
513
518
|
const [, , operator] = operatorMatch;
|
|
514
519
|
if (rt.dangerousOperators.includes(`$${operator}`)) {
|
|
@@ -707,6 +712,24 @@ function handleBracketSyntax(rt, field, operators, parsedFilters, depth = 0) {
|
|
|
707
712
|
if (typeof parsedFilters[field] === "object" && Object.keys(parsedFilters[field]).length === 0) delete parsedFilters[field];
|
|
708
713
|
}
|
|
709
714
|
/** Parse `?or=[...]` / `?OR=[...]` / `?$or=[...]` groups into an $or array. */
|
|
715
|
+
/**
|
|
716
|
+
* Parse the branches of a compound operator (`$and`, and the body of `$or`).
|
|
717
|
+
*
|
|
718
|
+
* Every branch goes back through `parseFilters`, so the operator block, the allowlist and the
|
|
719
|
+
* regex budget all apply inside it. Branches that parse to `{}` are DROPPED: a branch holding
|
|
720
|
+
* only a blocked operator would otherwise become match-all, which widens the query instead of
|
|
721
|
+
* failing it — the exact shape `parseOr` documents below.
|
|
722
|
+
*/
|
|
723
|
+
function parseCompoundBranches(rt, raw, depth) {
|
|
724
|
+
const items = Array.isArray(raw) ? raw : typeof raw === "object" && raw ? Object.values(raw) : [];
|
|
725
|
+
const branches = [];
|
|
726
|
+
for (const item of items) {
|
|
727
|
+
if (typeof item !== "object" || !item) continue;
|
|
728
|
+
const parsed = parseFilters(rt, item, depth + 1);
|
|
729
|
+
if (Object.keys(parsed).length > 0) branches.push(parsed);
|
|
730
|
+
}
|
|
731
|
+
return branches;
|
|
732
|
+
}
|
|
710
733
|
function parseOr(rt, query) {
|
|
711
734
|
const orArray = [];
|
|
712
735
|
const raw = query?.or || query?.OR || query?.$or;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/indexes/verify-indexes.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Compare DECLARED indexes against what a database actually has.
|
|
4
|
+
*
|
|
5
|
+
* `unique: true` on a schema is a correctness constraint only if the index was
|
|
6
|
+
* built. A skipped deploy step is otherwise SILENT — one deployment ran with
|
|
7
|
+
* 401 declared indexes absent, accumulated duplicate rows on a "unique" field,
|
|
8
|
+
* and nothing anywhere said the constraint did not exist. mongokit owns the
|
|
9
|
+
* declaration, so it owns the means to check it.
|
|
10
|
+
*
|
|
11
|
+
* Read-only. It NEVER builds: index builds belong to a deploy step, not to a
|
|
12
|
+
* serving process.
|
|
13
|
+
*
|
|
14
|
+
* Missing UNIQUE indexes are reported separately from missing regular ones —
|
|
15
|
+
* the first silently admits duplicate data, the second only costs a scan.
|
|
16
|
+
*
|
|
17
|
+
* DELIBERATELY ONE-DIRECTIONAL: an index present in the database but absent
|
|
18
|
+
* from a schema is NOT drift. Auth libraries and migrations manage indexes
|
|
19
|
+
* outside the ODM, and treating those as removable is exactly what makes a
|
|
20
|
+
* `syncIndexes()` unsafe.
|
|
21
|
+
*
|
|
22
|
+
* Sibling of `query/primitives/indexes`, which reads DECLARATIONS only and
|
|
23
|
+
* touches no connection. This module is the live comparison.
|
|
24
|
+
*/
|
|
25
|
+
/** The slice of a Mongoose-like connection this needs. Structural on purpose. */
|
|
26
|
+
interface IndexVerifiableConnection {
|
|
27
|
+
modelNames(): string[];
|
|
28
|
+
model(name: string): IndexVerifiableModel;
|
|
29
|
+
}
|
|
30
|
+
interface IndexVerifiableModel {
|
|
31
|
+
schema: {
|
|
32
|
+
indexes(): Array<[Record<string, unknown>, Record<string, unknown>?]>;
|
|
33
|
+
};
|
|
34
|
+
collection: {
|
|
35
|
+
name: string;
|
|
36
|
+
listIndexes(): {
|
|
37
|
+
toArray(): Promise<Array<Record<string, unknown>>>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
interface MissingIndex {
|
|
42
|
+
model: string;
|
|
43
|
+
collection: string;
|
|
44
|
+
/** The declared key, JSON-stringified — stable to log and to diff. */
|
|
45
|
+
key: string;
|
|
46
|
+
}
|
|
47
|
+
interface IndexVerifyReport {
|
|
48
|
+
modelsChecked: number;
|
|
49
|
+
/** Declared `unique` indexes with nothing behind them. Duplicates can already exist. */
|
|
50
|
+
missingUnique: MissingIndex[];
|
|
51
|
+
missingRegular: MissingIndex[];
|
|
52
|
+
/** Models whose indexes could not be read — collection absent, or no permission. */
|
|
53
|
+
unreadable: string[];
|
|
54
|
+
}
|
|
55
|
+
interface VerifyIndexesOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Include a model in the sweep. Default: every model.
|
|
58
|
+
*
|
|
59
|
+
* A kernel that materialises different model sets per mode must skip exactly
|
|
60
|
+
* the set its deploy step skips — indexing a model CREATES its collection, so
|
|
61
|
+
* a mismatch here reports permanent phantom drift instead of a real gap.
|
|
62
|
+
*/
|
|
63
|
+
includeModel?: (modelName: string) => boolean;
|
|
64
|
+
}
|
|
65
|
+
declare function verifyIndexes(connection: IndexVerifiableConnection, options?: VerifyIndexesOptions): Promise<IndexVerifyReport>;
|
|
66
|
+
/** One-line summary for a boot log. */
|
|
67
|
+
declare function formatIndexReport(report: IndexVerifyReport): string;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { IndexVerifiableConnection, IndexVerifiableModel, IndexVerifyReport, MissingIndex, VerifyIndexesOptions, formatIndexReport, verifyIndexes };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/indexes/verify-indexes.ts
|
|
2
|
+
/**
|
|
3
|
+
* Order-sensitive: a compound index is not the same index with keys swapped.
|
|
4
|
+
*
|
|
5
|
+
* A SINGLE-field numeric index is direction-agnostic though — mongo traverses
|
|
6
|
+
* an index either way, so `{t:1}` and `{t:-1}` are the same index and a schema
|
|
7
|
+
* declaring one against a database holding the other is not drift. Only numeric
|
|
8
|
+
* directions collapse; `2dsphere` / `hashed` are index TYPES, not directions.
|
|
9
|
+
*/
|
|
10
|
+
function normalizeKey(key) {
|
|
11
|
+
const entries = Object.entries(key);
|
|
12
|
+
if (entries.length === 1 && typeof entries[0]?.[1] === "number") return JSON.stringify([[entries[0][0]]]);
|
|
13
|
+
return JSON.stringify(entries);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A TEXT index is stored under a key mongo invents (`{_fts, _ftsx}`) with the
|
|
17
|
+
* real fields moved to `weights`, so it never matches its own declaration by
|
|
18
|
+
* key. Compare the field SET instead, or every text index reads as permanently
|
|
19
|
+
* missing and the whole check becomes noise nobody reads.
|
|
20
|
+
*/
|
|
21
|
+
function textFields(key) {
|
|
22
|
+
const fields = Object.entries(key).filter(([, dir]) => dir === "text").map(([field]) => field);
|
|
23
|
+
return fields.length > 0 ? fields.sort() : null;
|
|
24
|
+
}
|
|
25
|
+
async function verifyIndexes(connection, options = {}) {
|
|
26
|
+
const include = options.includeModel ?? (() => true);
|
|
27
|
+
const report = {
|
|
28
|
+
modelsChecked: 0,
|
|
29
|
+
missingUnique: [],
|
|
30
|
+
missingRegular: [],
|
|
31
|
+
unreadable: []
|
|
32
|
+
};
|
|
33
|
+
for (const name of connection.modelNames().sort()) {
|
|
34
|
+
if (!include(name)) continue;
|
|
35
|
+
const model = connection.model(name);
|
|
36
|
+
const declared = model.schema.indexes();
|
|
37
|
+
if (declared.length === 0) continue;
|
|
38
|
+
let existing;
|
|
39
|
+
let existingText;
|
|
40
|
+
try {
|
|
41
|
+
const live = await model.collection.listIndexes().toArray();
|
|
42
|
+
existing = new Set(live.map((i) => normalizeKey(i.key)));
|
|
43
|
+
existingText = new Set(live.filter((i) => i.weights).map((i) => JSON.stringify(Object.keys(i.weights).sort())));
|
|
44
|
+
} catch {
|
|
45
|
+
report.unreadable.push(name);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
report.modelsChecked += 1;
|
|
49
|
+
for (const [key, indexOptions] of declared) {
|
|
50
|
+
const asText = textFields(key);
|
|
51
|
+
if (asText) {
|
|
52
|
+
if (existingText.has(JSON.stringify(asText))) continue;
|
|
53
|
+
} else if (existing.has(normalizeKey(key))) continue;
|
|
54
|
+
const entry = {
|
|
55
|
+
model: name,
|
|
56
|
+
collection: model.collection.name,
|
|
57
|
+
key: JSON.stringify(key)
|
|
58
|
+
};
|
|
59
|
+
if (indexOptions?.unique) report.missingUnique.push(entry);
|
|
60
|
+
else report.missingRegular.push(entry);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return report;
|
|
64
|
+
}
|
|
65
|
+
/** One-line summary for a boot log. */
|
|
66
|
+
function formatIndexReport(report) {
|
|
67
|
+
const parts = [
|
|
68
|
+
`${report.modelsChecked} models checked`,
|
|
69
|
+
`missing unique: ${report.missingUnique.length}`,
|
|
70
|
+
`missing regular: ${report.missingRegular.length}`
|
|
71
|
+
];
|
|
72
|
+
if (report.unreadable.length > 0) parts.push(`unreadable: ${report.unreadable.length}`);
|
|
73
|
+
return `[index-check] ${parts.join(" — ")}`;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { formatIndexReport, verifyIndexes };
|
package/dist/outbox/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Repository } from "../Repository-
|
|
1
|
+
import { n as Repository } from "../Repository-Ck5x43wF.mjs";
|
|
2
2
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.mjs";
|
|
3
3
|
import { Schema } from "mongoose";
|
|
4
4
|
//#region src/outbox/index.ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as PaginationEngine } from "../PaginationEngine-
|
|
1
|
+
import { t as PaginationEngine } from "../PaginationEngine-BX9o_-4R.mjs";
|
|
2
2
|
export { PaginationEngine };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { c as PaginationConfig } from "../../pagination-jSUdrACK.mjs";
|
|
2
|
+
//#region src/pagination/utils/limits.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Validates and sanitizes limit value
|
|
5
|
+
* Parses strings to numbers and prevents NaN bugs
|
|
6
|
+
*
|
|
7
|
+
* @param limit - Requested limit
|
|
8
|
+
* @param config - Pagination configuration
|
|
9
|
+
* @returns Sanitized limit between 1 and maxLimit
|
|
10
|
+
*/
|
|
11
|
+
declare function validateLimit(limit: number | string, config: PaginationConfig): number;
|
|
12
|
+
/**
|
|
13
|
+
* Validates and sanitizes page number
|
|
14
|
+
* Parses strings to numbers and prevents NaN bugs
|
|
15
|
+
*
|
|
16
|
+
* @param page - Requested page (1-indexed)
|
|
17
|
+
* @param config - Pagination configuration
|
|
18
|
+
* @returns Sanitized page number >= 1
|
|
19
|
+
* @throws Error if page exceeds maxPage
|
|
20
|
+
*/
|
|
21
|
+
declare function validatePage(page: number | string, config: PaginationConfig): number;
|
|
22
|
+
/**
|
|
23
|
+
* Checks if page number should trigger deep pagination warning
|
|
24
|
+
*
|
|
25
|
+
* @param page - Current page number
|
|
26
|
+
* @param threshold - Warning threshold
|
|
27
|
+
* @returns True if warning should be shown
|
|
28
|
+
*/
|
|
29
|
+
declare function shouldWarnDeepPagination(page: number, threshold: number): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Calculates number of documents to skip for offset pagination
|
|
32
|
+
*
|
|
33
|
+
* @param page - Page number (1-indexed)
|
|
34
|
+
* @param limit - Documents per page
|
|
35
|
+
* @returns Number of documents to skip
|
|
36
|
+
*/
|
|
37
|
+
declare function calculateSkip(page: number, limit: number): number;
|
|
38
|
+
/**
|
|
39
|
+
* Calculates total number of pages
|
|
40
|
+
*
|
|
41
|
+
* @param total - Total document count
|
|
42
|
+
* @param limit - Documents per page
|
|
43
|
+
* @returns Total number of pages
|
|
44
|
+
*/
|
|
45
|
+
declare function calculateTotalPages(total: number, limit: number): number;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/pagination/utils/limits.ts
|
|
2
|
+
/**
|
|
3
|
+
* Validates and sanitizes limit value
|
|
4
|
+
* Parses strings to numbers and prevents NaN bugs
|
|
5
|
+
*
|
|
6
|
+
* @param limit - Requested limit
|
|
7
|
+
* @param config - Pagination configuration
|
|
8
|
+
* @returns Sanitized limit between 1 and maxLimit
|
|
9
|
+
*/
|
|
10
|
+
function validateLimit(limit, config) {
|
|
11
|
+
const parsed = Number(limit);
|
|
12
|
+
if (!Number.isFinite(parsed) || parsed < 1) return config.defaultLimit ?? 10;
|
|
13
|
+
const max = config.maxLimit ?? 100;
|
|
14
|
+
if (max === 0) return Math.floor(parsed);
|
|
15
|
+
return Math.min(Math.floor(parsed), max);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validates and sanitizes page number
|
|
19
|
+
* Parses strings to numbers and prevents NaN bugs
|
|
20
|
+
*
|
|
21
|
+
* @param page - Requested page (1-indexed)
|
|
22
|
+
* @param config - Pagination configuration
|
|
23
|
+
* @returns Sanitized page number >= 1
|
|
24
|
+
* @throws Error if page exceeds maxPage
|
|
25
|
+
*/
|
|
26
|
+
function validatePage(page, config) {
|
|
27
|
+
const parsed = Number(page);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed < 1) return 1;
|
|
29
|
+
const sanitized = Math.floor(parsed);
|
|
30
|
+
if (sanitized > (config.maxPage || 1e4)) throw new Error(`Page ${sanitized} exceeds maximum ${config.maxPage || 1e4}`);
|
|
31
|
+
return sanitized;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks if page number should trigger deep pagination warning
|
|
35
|
+
*
|
|
36
|
+
* @param page - Current page number
|
|
37
|
+
* @param threshold - Warning threshold
|
|
38
|
+
* @returns True if warning should be shown
|
|
39
|
+
*/
|
|
40
|
+
function shouldWarnDeepPagination(page, threshold) {
|
|
41
|
+
return page > threshold;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Calculates number of documents to skip for offset pagination
|
|
45
|
+
*
|
|
46
|
+
* @param page - Page number (1-indexed)
|
|
47
|
+
* @param limit - Documents per page
|
|
48
|
+
* @returns Number of documents to skip
|
|
49
|
+
*/
|
|
50
|
+
function calculateSkip(page, limit) {
|
|
51
|
+
return (page - 1) * limit;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Calculates total number of pages
|
|
55
|
+
*
|
|
56
|
+
* @param total - Total document count
|
|
57
|
+
* @param limit - Documents per page
|
|
58
|
+
* @returns Total number of pages
|
|
59
|
+
*/
|
|
60
|
+
function calculateTotalPages(total, limit) {
|
|
61
|
+
return Math.ceil(total / limit);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "../timestamp.plugin-
|
|
1
|
+
import { a as customIdPlugin, c as prefixedId, d as AuditTrailQuery, f as auditTrailPlugin, i as multiTenantPlugin, l as sequentialId, m as auditLogPlugin, n as softDeletePlugin, o as dateSequentialId, p as ensureAuditTrailReady, s as getNextSequence, t as timestampPlugin, u as cachePlugin } from "../timestamp.plugin-CBQukQzV.mjs";
|
|
2
2
|
import { n as batchOperationsPlugin, t as methodRegistryPlugin } from "../method-registry.plugin-DblM7V6j.mjs";
|
|
3
|
-
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-
|
|
3
|
+
import { _ as cascadePlugin, a as uniqueField, c as subdocumentPlugin, d as mongoOperationsPlugin, f as leasePlugin, g as elasticSearchPlugin, h as fieldFilterPlugin, i as requireField, l as observabilityPlugin, m as idempotencyStorePlugin, n as blockIf, o as validationChainPlugin, p as immutableStatesPlugin, r as immutableField, s as createTenantContext, t as autoInject, u as MONGOKIT_PLUGIN_NAMES, v as appendOnlyPlugin, y as aggregateHelpersPlugin } from "../validation-chain.plugin-DhNyxNCw.mjs";
|
|
4
4
|
//#region src/plugins/change-log.plugin.ts
|
|
5
5
|
const PLUGIN_NAME = "changeLog";
|
|
6
6
|
function isSkipped(context) {
|
package/dist/testkit/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-
|
|
1
|
+
import { c as ALL_OPERATIONS, t as HOOK_PRIORITY, u as OP_REGISTRY } from "./Repository-Ck5x43wF.mjs";
|
|
2
2
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
3
3
|
import { throwIfAborted, withRetry } from "@classytic/repo-core/repository";
|
|
4
4
|
import mongoose from "mongoose";
|
|
@@ -1290,6 +1290,50 @@ function softDeletePlugin(options = {}) {
|
|
|
1290
1290
|
};
|
|
1291
1291
|
}
|
|
1292
1292
|
}, { priority: HOOK_PRIORITY.POLICY });
|
|
1293
|
+
/**
|
|
1294
|
+
* Hook: before:bulkWrite — every sub-op gets what its standalone form gets.
|
|
1295
|
+
*
|
|
1296
|
+
* This was skipped as "not a single filter we can scope", but a sub-op DOES carry
|
|
1297
|
+
* its own filter — the tenant plugin walks exactly these keys. Skipping it meant a
|
|
1298
|
+
* soft-delete repository's `bulkWrite` updated rows it had already deleted, and
|
|
1299
|
+
* HARD-deleted through `deleteOne`/`deleteMany`, destroying data the model says is
|
|
1300
|
+
* recoverable. The suite's own header claimed this was covered.
|
|
1301
|
+
*
|
|
1302
|
+
* `deleteMode: 'hard'` still bypasses, exactly as the single-doc path does — a purge
|
|
1303
|
+
* is a deliberate physical delete, and it reaches the driver directly anyway.
|
|
1304
|
+
*/
|
|
1305
|
+
repo.on("before:bulkWrite", (context) => {
|
|
1306
|
+
if (options.soft === false || context.deleteMode === "hard") return;
|
|
1307
|
+
const ops = context.operations;
|
|
1308
|
+
if (!Array.isArray(ops)) return;
|
|
1309
|
+
const notDeleted = buildDeletedFilter(deletedField, filterMode, false);
|
|
1310
|
+
for (const subOp of ops) {
|
|
1311
|
+
if (!subOp || typeof subOp !== "object") continue;
|
|
1312
|
+
for (const key of [
|
|
1313
|
+
"updateOne",
|
|
1314
|
+
"updateMany",
|
|
1315
|
+
"replaceOne"
|
|
1316
|
+
]) {
|
|
1317
|
+
const body = subOp[key];
|
|
1318
|
+
if (body?.filter) body.filter = {
|
|
1319
|
+
...body.filter,
|
|
1320
|
+
...notDeleted
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
for (const [key, target] of [["deleteOne", "updateOne"], ["deleteMany", "updateMany"]]) {
|
|
1324
|
+
const body = subOp[key];
|
|
1325
|
+
if (!body?.filter) continue;
|
|
1326
|
+
subOp[target] = {
|
|
1327
|
+
filter: {
|
|
1328
|
+
...body.filter,
|
|
1329
|
+
...notDeleted
|
|
1330
|
+
},
|
|
1331
|
+
update: { $set: { [deletedField]: /* @__PURE__ */ new Date() } }
|
|
1332
|
+
};
|
|
1333
|
+
delete subOp[key];
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}, { priority: HOOK_PRIORITY.POLICY });
|
|
1293
1337
|
repo.on("before:deleteMany", async (context) => {
|
|
1294
1338
|
if (context.deleteMode === "hard") return;
|
|
1295
1339
|
if (options.soft !== false) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as collectIds, r as idChunks, t as DEFAULT_ID_CHUNK } from "./id-chunks-BmAwsqNk.mjs";
|
|
2
|
-
import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-
|
|
2
|
+
import { c as ALL_OPERATIONS, u as OP_REGISTRY } from "./Repository-Ck5x43wF.mjs";
|
|
3
3
|
import { _ as upsert } from "./update-jSXp3nu3.mjs";
|
|
4
4
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
5
5
|
import { t as createError } from "./error-BuByZwKU.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/mongokit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.1",
|
|
4
4
|
"description": "Production-grade MongoDB repositories - smart pagination, events, and plugins. Built on @classytic/repo-core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/pagination/PaginationEngine.d.mts",
|
|
17
17
|
"default": "./dist/pagination/PaginationEngine.mjs"
|
|
18
18
|
},
|
|
19
|
+
"./pagination/limits": {
|
|
20
|
+
"types": "./dist/pagination/utils/limits.d.mts",
|
|
21
|
+
"default": "./dist/pagination/utils/limits.mjs"
|
|
22
|
+
},
|
|
19
23
|
"./plugins": {
|
|
20
24
|
"types": "./dist/plugins/index.d.mts",
|
|
21
25
|
"default": "./dist/plugins/index.mjs"
|
|
@@ -64,6 +68,10 @@
|
|
|
64
68
|
"types": "./dist/query/primitives/indexes.d.mts",
|
|
65
69
|
"default": "./dist/query/primitives/indexes.mjs"
|
|
66
70
|
},
|
|
71
|
+
"./indexes": {
|
|
72
|
+
"types": "./dist/indexes/verify-indexes.d.mts",
|
|
73
|
+
"default": "./dist/indexes/verify-indexes.mjs"
|
|
74
|
+
},
|
|
67
75
|
"./testkit": {
|
|
68
76
|
"types": "./dist/testkit/index.d.mts",
|
|
69
77
|
"default": "./dist/testkit/index.mjs"
|
|
@@ -122,7 +130,7 @@
|
|
|
122
130
|
},
|
|
123
131
|
"homepage": "https://github.com/classytic/mongokit#readme",
|
|
124
132
|
"peerDependencies": {
|
|
125
|
-
"@classytic/repo-core": ">=0.
|
|
133
|
+
"@classytic/repo-core": ">=0.27.0",
|
|
126
134
|
"mongodb-memory-server": ">=10.4.3",
|
|
127
135
|
"mongoose": ">=9.4.1"
|
|
128
136
|
},
|
|
@@ -174,7 +182,7 @@
|
|
|
174
182
|
"@better-auth/mongo-adapter": "^1.6.29",
|
|
175
183
|
"@biomejs/biome": "^2.5.8",
|
|
176
184
|
"@classytic/dev-tools": "^0.2.0",
|
|
177
|
-
"@classytic/repo-core": "^0.
|
|
185
|
+
"@classytic/repo-core": "^0.27.0",
|
|
178
186
|
"@types/node": "^26.2.0",
|
|
179
187
|
"@vitest/coverage-v8": "^4.1.10",
|
|
180
188
|
"ajv": "^8.20.0",
|