@classytic/mongokit 3.12.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -4
- package/dist/{PaginationEngine-BDaAsyPn.mjs → PaginationEngine-CfQRM1yz.mjs} +16 -16
- package/dist/Repository-BY40uhcv.mjs +3603 -0
- package/dist/actions/index.d.mts +1 -1
- package/dist/actions/index.mjs +1 -1
- package/dist/adapter/index.d.mts +190 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter-DTDMnUOH.mjs +158 -0
- package/dist/ai/index.d.mts +1 -1
- package/dist/ai/index.mjs +1 -1
- package/dist/better-auth/index.d.mts +149 -0
- package/dist/better-auth/index.mjs +81 -0
- package/dist/field-selection-Br3WPOEb.mjs +99 -0
- package/dist/{index-D9cORR-o.d.mts → index-BbHaKwi5.d.mts} +33 -13
- package/dist/index.d.mts +667 -30
- package/dist/index.mjs +240 -17
- package/dist/{logger-8-VS1za6.mjs → logger-DprijP5h.mjs} +1 -5
- package/dist/{mongooseToJsonSchema-B187ufOP.mjs → mongooseToJsonSchema-BYbG894y.mjs} +1 -1
- package/dist/{mongooseToJsonSchema-rtkdbBhf.d.mts → mongooseToJsonSchema-BgRAaXX-.d.mts} +2 -1
- package/dist/pagination/PaginationEngine.d.mts +2 -2
- package/dist/pagination/PaginationEngine.mjs +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +2 -2
- package/dist/query/primitives/coercion.d.mts +1 -1
- package/dist/query/primitives/coercion.mjs +1 -1
- package/dist/query/primitives/indexes.d.mts +1 -1
- package/dist/{types-DXscZnLu.d.mts → types-BbaPZleN.d.mts} +371 -112
- package/dist/{update-CDxPtpsk.mjs → update-DLcSSa5b.mjs} +41 -27
- package/dist/utils/index.d.mts +2 -91
- package/dist/utils/index.mjs +5 -5
- package/dist/{validation-chain.plugin-CH-xPoKM.d.mts → validation-chain.plugin-CAPYCj_T.d.mts} +212 -80
- package/dist/validation-chain.plugin-_-WuNml2.mjs +2399 -0
- package/package.json +20 -3
- package/dist/field-selection-dI0m4RBs.mjs +0 -221
- package/dist/validation-chain.plugin-BicIgzzt.mjs +0 -4970
- /package/dist/{coercion-oM8G4b6m.d.mts → coercion-TRMmR49h.d.mts} +0 -0
- /package/dist/{id-resolution-e4BT9zAV.mjs → id-resolution-B0Odvvou.mjs} +0 -0
- /package/dist/{indexes-C26GNZRU.d.mts → indexes-CgOF7Wjd.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Production-grade MongoDB repository pattern for Node.js. Zero runtime deps — M
|
|
|
9
9
|
npm install @classytic/mongokit @classytic/repo-core mongoose
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Requires Mongoose `>=9.4.1`, `@classytic/repo-core` `>=0.
|
|
12
|
+
Requires Mongoose `>=9.4.1`, `@classytic/repo-core` `>=0.4.0`, Node.js `>=22`.
|
|
13
13
|
|
|
14
14
|
> **Swap-able with sqlitekit.** Mongokit implements the `StandardRepo<TDoc>` contract from `@classytic/repo-core/repository`. Controller code written against the contract runs unchanged on [@classytic/sqlitekit](https://www.npmjs.com/package/@classytic/sqlitekit) — both kits share an identical conformance suite.
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ Requires Mongoose `>=9.4.1`, `@classytic/repo-core` `>=0.3.0`, Node.js `>=22`.
|
|
|
17
17
|
|
|
18
18
|
- `getById(id)` → returns `null` on miss (not thrown). Invalid-shape ids (e.g. `'not-a-valid-id'` on an ObjectId `_id`) short-circuit to `null` rather than raising mongoose `CastError`.
|
|
19
19
|
- `update(id, data)` → returns `null` on miss.
|
|
20
|
-
- `delete(id)` → returns `
|
|
20
|
+
- `delete(id)` → returns `null` on miss. On hit, returns a `DeleteResult` (e.g. `{ message: 'Soft deleted successfully', id, soft: true }` when `softDeletePlugin` is wired; the driver's hard-delete result otherwise).
|
|
21
21
|
- Pass `{ throwOnNotFound: true }` to opt back into the legacy 404-throw behavior for any of the three.
|
|
22
22
|
|
|
23
23
|
---
|
|
@@ -96,6 +96,42 @@ repo.on('after:create', ({ context, result }) => { /* audit, notify */ });
|
|
|
96
96
|
|
|
97
97
|
The `before:*` hook receives the context directly; `after:*` and `error:*` receive `{ context, result | error }`.
|
|
98
98
|
|
|
99
|
+
### Middleware
|
|
100
|
+
|
|
101
|
+
`useMiddleware()` composes around every op (including cache-hit branches), wrap-style. Inputs and outputs both pass through; registration order = composition order (first registered runs outermost).
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
repo.useMiddleware(async ({ operation, next }) => {
|
|
105
|
+
const start = performance.now();
|
|
106
|
+
try { return await next(); }
|
|
107
|
+
finally { metrics.record(operation, performance.now() - start); }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Short-circuit by returning without calling next()
|
|
111
|
+
repo.useMiddleware(async ({ operation, context, next }) => {
|
|
112
|
+
if (operation === 'getById' && readOnlyMaintenance) return cachedReadOnlyResponse(context.id);
|
|
113
|
+
return next();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Middleware is for ergonomics, NOT security.** This is the load-bearing distinction. The execution order on every call is:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
_buildContext + before:<op> ← repo.on('before:*') hooks
|
|
121
|
+
[outer middleware pre] ← repo.useMiddleware() registrations
|
|
122
|
+
[...inner middleware pre]
|
|
123
|
+
fn (driver call)
|
|
124
|
+
after:<op> | error:<op> ← repo.on('after:*' / 'error:*')
|
|
125
|
+
[...inner middleware post]
|
|
126
|
+
[outer middleware post]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Build/`before:*` hooks fire BEFORE the middleware chain dispatches. A throw from a `before:*` policy hook (tenant scope, soft-delete, access-control) unwinds before middleware ever fires — middleware **cannot wrap, observe, or short-circuit a policy failure**.
|
|
130
|
+
|
|
131
|
+
That's by design. Middleware as a security boundary would be impossible to audit because registration order would determine whether tenant scope wins. Use `before:*` hooks for security policy (tenant scope, soft-delete filtering, audit, cache invalidation) and `useMiddleware()` for ergonomics (timing, metrics, input/output mutation in a single closure).
|
|
132
|
+
|
|
133
|
+
If you want middleware to observe a policy rejection, listen on `error:<op>` — that fires from inside the middleware chain and is reachable.
|
|
134
|
+
|
|
99
135
|
---
|
|
100
136
|
|
|
101
137
|
## Pagination
|
|
@@ -105,12 +141,12 @@ The `before:*` hook receives the context directly; `after:*` and `error:*` recei
|
|
|
105
141
|
```ts
|
|
106
142
|
// Offset — dashboards, admin panels
|
|
107
143
|
await repo.getAll({ mode: 'offset', page: 1, limit: 20, sort: { createdAt: -1 } });
|
|
108
|
-
// → { method: 'offset',
|
|
144
|
+
// → { method: 'offset', data, total, pages, hasNext, hasPrev, page, limit }
|
|
109
145
|
|
|
110
146
|
// Keyset — feeds, infinite scroll
|
|
111
147
|
const p1 = await repo.getAll({ mode: 'keyset', sort: { createdAt: -1 }, limit: 20 });
|
|
112
148
|
const p2 = await repo.getAll({ mode: 'keyset', sort: { createdAt: -1 }, after: p1.next });
|
|
113
|
-
// → { method: 'keyset',
|
|
149
|
+
// → { method: 'keyset', data, hasMore, next, limit }
|
|
114
150
|
```
|
|
115
151
|
|
|
116
152
|
Keyset pagination with `filters + sort` warns once if no matching schema-declared compound index exists. Silent in `NODE_ENV=test`. Route warnings via `configureLogger({ warn })`.
|
|
@@ -313,6 +349,40 @@ import { coerceFieldValue } from '@classytic/mongokit/query/primitives/coercion'
|
|
|
313
349
|
|
|
314
350
|
---
|
|
315
351
|
|
|
352
|
+
## Better Auth bridge — `@classytic/mongokit/better-auth`
|
|
353
|
+
|
|
354
|
+
When [Better Auth](https://better-auth.com) writes to your Mongo collections via `@better-auth/mongo-adapter`, mongokit ships a kit-owned bridge so you can expose those collections as full repositories — pagination, query parser, OpenAPI, audit, all of it.
|
|
355
|
+
|
|
356
|
+
**Two helpers**:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import {
|
|
360
|
+
registerBetterAuthStubs, // bulk stubs for `populate('user')` etc.
|
|
361
|
+
createBetterAuthOverlay, // per-collection DataAdapter for arc/your host
|
|
362
|
+
} from '@classytic/mongokit/better-auth';
|
|
363
|
+
|
|
364
|
+
// 1. Bulk-register stubs so populate() works app-wide.
|
|
365
|
+
registerBetterAuthStubs(mongoose, { plugins: ['organization'] });
|
|
366
|
+
|
|
367
|
+
// 2. Per-resource overlay — async because we read BA's resolved schema.
|
|
368
|
+
// Resolves once at boot, picks up additionalFields + modelName overrides
|
|
369
|
+
// + plugin schema additions automatically.
|
|
370
|
+
const orgAdapter = await createBetterAuthOverlay({
|
|
371
|
+
auth, // your betterAuth() instance
|
|
372
|
+
mongoose,
|
|
373
|
+
collection: 'organization',
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Plug the adapter into any host that consumes `DataAdapter<TDoc>`:
|
|
377
|
+
defineResource({ name: 'organization', adapter: orgAdapter });
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Need custom validators / Repository methods / `toJSON` transforms (e.g., strip `password`)? Drop the factory and hand-roll the schema + Repository — that's the recommended path when you outgrow the defaults. The factory and the hand-roll produce structurally identical adapters; you can switch back and forth without touching consumer code.
|
|
381
|
+
|
|
382
|
+
`better-auth` is an **optional peer dependency** — only required when you import this subpath. Mongokit users who don't touch BA don't get an install warning.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
316
386
|
## Testing
|
|
317
387
|
|
|
318
388
|
Uses `mongodb-memory-server` by default; override via `MONGODB_URI` for a real replica set when running transaction tests locally.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as warn } from "./logger-DprijP5h.mjs";
|
|
2
2
|
import { t as createError } from "./error-B00pI7dV.mjs";
|
|
3
3
|
import { n as encodeCursor, r as resolveCursorFilter } from "./cursor-J84rR6wG.mjs";
|
|
4
4
|
import { r as validateKeysetSort, t as getPrimaryField } from "./sort-CrZjTK25.mjs";
|
|
@@ -224,7 +224,7 @@ var PaginationEngine = class {
|
|
|
224
224
|
* page: 1,
|
|
225
225
|
* limit: 20
|
|
226
226
|
* });
|
|
227
|
-
* console.log(result.
|
|
227
|
+
* console.log(result.data, result.total, result.hasNext);
|
|
228
228
|
*/
|
|
229
229
|
async paginate(options = {}) {
|
|
230
230
|
const { filters = {}, sort, countFilters, page = 1, limit = this.config.defaultLimit, select, populate = [], lean = true, session, hint, maxTimeMS, countStrategy = "exact", readPreference, collation } = options;
|
|
@@ -255,17 +255,17 @@ var PaginationEngine = class {
|
|
|
255
255
|
if (readPreference) countQuery.read(readPreference);
|
|
256
256
|
countPromise = countQuery.exec();
|
|
257
257
|
}
|
|
258
|
-
const [
|
|
258
|
+
const [data, total] = await Promise.all([query.exec(), countPromise]);
|
|
259
259
|
const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
|
|
260
260
|
let hasNext;
|
|
261
261
|
if (countStrategy === "none") {
|
|
262
|
-
hasNext =
|
|
263
|
-
if (hasNext)
|
|
262
|
+
hasNext = data.length > sanitizedLimit;
|
|
263
|
+
if (hasNext) data.pop();
|
|
264
264
|
} else hasNext = sanitizedPage < totalPages;
|
|
265
265
|
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination (page ${sanitizedPage}). Consider getAll({ after, sort, limit }) for better performance.` : void 0;
|
|
266
266
|
return {
|
|
267
267
|
method: "offset",
|
|
268
|
-
|
|
268
|
+
data,
|
|
269
269
|
page: sanitizedPage,
|
|
270
270
|
limit: sanitizedLimit,
|
|
271
271
|
total,
|
|
@@ -321,16 +321,16 @@ var PaginationEngine = class {
|
|
|
321
321
|
if (hint) mongoQuery = mongoQuery.hint(hint);
|
|
322
322
|
if (maxTimeMS) mongoQuery = mongoQuery.maxTimeMS(maxTimeMS);
|
|
323
323
|
if (readPreference) mongoQuery = mongoQuery.read(readPreference);
|
|
324
|
-
const
|
|
325
|
-
const hasMore =
|
|
326
|
-
if (hasMore)
|
|
324
|
+
const data = await mongoQuery.exec();
|
|
325
|
+
const hasMore = data.length > sanitizedLimit;
|
|
326
|
+
if (hasMore) data.pop();
|
|
327
327
|
const primaryField = getPrimaryField(normalizedSort);
|
|
328
328
|
return {
|
|
329
329
|
method: "keyset",
|
|
330
|
-
|
|
330
|
+
data,
|
|
331
331
|
limit: sanitizedLimit,
|
|
332
332
|
hasMore,
|
|
333
|
-
next: hasMore &&
|
|
333
|
+
next: hasMore && data.length > 0 ? encodeCursor(data[data.length - 1], primaryField, normalizedSort, this.config.cursorVersion) : null
|
|
334
334
|
};
|
|
335
335
|
}
|
|
336
336
|
/**
|
|
@@ -358,7 +358,7 @@ var PaginationEngine = class {
|
|
|
358
358
|
const sanitizedLimit = validateLimit(limit, this.config);
|
|
359
359
|
const skip = calculateSkip(sanitizedPage, sanitizedLimit);
|
|
360
360
|
const fetchLimit = countStrategy === "none" ? sanitizedLimit + 1 : sanitizedLimit;
|
|
361
|
-
const facetStages = {
|
|
361
|
+
const facetStages = { data: [{ $skip: skip }, { $limit: fetchLimit }] };
|
|
362
362
|
if (countStrategy !== "none") facetStages.total = [{ $count: "count" }];
|
|
363
363
|
const facetPipeline = [...pipeline, { $facet: facetStages }];
|
|
364
364
|
const aggregation = this.Model.aggregate(facetPipeline);
|
|
@@ -367,18 +367,18 @@ var PaginationEngine = class {
|
|
|
367
367
|
if (maxTimeMS) aggregation.option({ maxTimeMS });
|
|
368
368
|
if (readPreference) aggregation.read(readPreference);
|
|
369
369
|
const [result] = await aggregation.exec();
|
|
370
|
-
const
|
|
370
|
+
const data = result.data;
|
|
371
371
|
const total = result.total?.[0]?.count || 0;
|
|
372
372
|
const totalPages = countStrategy === "none" ? 0 : calculateTotalPages(total, sanitizedLimit);
|
|
373
373
|
let hasNext;
|
|
374
374
|
if (countStrategy === "none") {
|
|
375
|
-
hasNext =
|
|
376
|
-
if (hasNext)
|
|
375
|
+
hasNext = data.length > sanitizedLimit;
|
|
376
|
+
if (hasNext) data.pop();
|
|
377
377
|
} else hasNext = sanitizedPage < totalPages;
|
|
378
378
|
const warning = shouldWarnDeepPagination(sanitizedPage, this.config.deepPageThreshold) ? `Deep pagination in aggregate (page ${sanitizedPage}). Uses $skip internally.` : void 0;
|
|
379
379
|
return {
|
|
380
380
|
method: "aggregate",
|
|
381
|
-
|
|
381
|
+
data,
|
|
382
382
|
page: sanitizedPage,
|
|
383
383
|
limit: sanitizedLimit,
|
|
384
384
|
total,
|