@classytic/mongokit 3.33.0 → 3.35.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/adapter/index.mjs +1 -1
- package/dist/{adapter-BitHg-CN.mjs → adapter-DlaOGcQN.mjs} +1 -1
- package/dist/better-auth/index.d.mts +60 -1
- package/dist/better-auth/index.mjs +55 -4
- package/dist/index.mjs +1 -1
- package/dist/{mongooseToJsonSchema-DKbnKduN.mjs → mongooseToJsonSchema-waUVQwK4.mjs} +29 -1
- package/dist/utils/index.mjs +1 -1
- package/package.json +3 -3
package/dist/adapter/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-
|
|
1
|
+
import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-DlaOGcQN.mjs";
|
|
2
2
|
export { MongooseAdapter, createMongooseAdapter, isMongooseModel };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as warn } from "./logger-K7dPmu2U.mjs";
|
|
2
|
-
import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-
|
|
2
|
+
import { t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-waUVQwK4.mjs";
|
|
3
3
|
import { matchesRecordFilter } from "@classytic/repo-core/filter";
|
|
4
4
|
import { mergeFieldRuleConstraints } from "@classytic/repo-core/schema";
|
|
5
5
|
import { asRepositoryLike, isRepository } from "@classytic/repo-core/adapter";
|
|
@@ -49,9 +49,32 @@ interface BAFieldAttribute {
|
|
|
49
49
|
};
|
|
50
50
|
defaultValue?: unknown;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* How Better Auth's `_id` values are stored, so mongoose casts queries the same way.
|
|
54
|
+
*
|
|
55
|
+
* - `'objectid'` (DEFAULT) — BA's mongo adapter generating its own ids writes
|
|
56
|
+
* real ObjectIds. Mongoose's default `_id` SchemaType casts a hex string to
|
|
57
|
+
* ObjectId, which is what makes `findById`, `_id` filters and `populate`
|
|
58
|
+
* resolve.
|
|
59
|
+
* - `'string'` — the host overrode `advanced.database.generateId` (commonly to
|
|
60
|
+
* `randomBytes(12).toString('hex')`, which AVOIDS a cross-driver ObjectId
|
|
61
|
+
* `instanceof` failure). BA then stores those ids as STRINGS.
|
|
62
|
+
*
|
|
63
|
+
* Getting this wrong fails in the quiet direction, in BOTH directions: an
|
|
64
|
+
* ObjectId-typed schema over string ids casts every lookup to an ObjectId that
|
|
65
|
+
* matches nothing, so `getById` 404s and `populate` yields null — no error, no
|
|
66
|
+
* log. Measured on a real deployment: `GET /me`, `GET /users/:id` and
|
|
67
|
+
* `GET /branches/code/:code` all 404'd for rows that existed.
|
|
68
|
+
*
|
|
69
|
+
* There is no safe default that covers both, which is why this is an explicit
|
|
70
|
+
* option rather than a guess: read `typeof doc._id` from a live row.
|
|
71
|
+
*/
|
|
72
|
+
type BetterAuthIdType = 'objectid' | 'string';
|
|
52
73
|
interface RegisterBetterAuthStubsOptions {
|
|
53
74
|
/** Plugin sets to include. `core` is always implied. */
|
|
54
75
|
plugins?: BetterAuthPluginKey$1[];
|
|
76
|
+
/** How BA stores `_id` — see {@link BetterAuthIdType}. Default `'objectid'`. */
|
|
77
|
+
idType?: BetterAuthIdType;
|
|
55
78
|
/** Additional collection names beyond the plugin set. */
|
|
56
79
|
extraCollections?: string[];
|
|
57
80
|
/** Mirror BA's `usePlural` flag — appends `s` to every collection name. */
|
|
@@ -115,6 +138,8 @@ interface BetterAuthOverlayOptions<TDoc = Record<string, unknown>> {
|
|
|
115
138
|
* Mongoose-side concerns (validators, indexes, defaults).
|
|
116
139
|
*/
|
|
117
140
|
additionalFields?: Record<string, unknown>;
|
|
141
|
+
/** How BA stores `_id` — see {@link BetterAuthIdType}. Default `'objectid'`. */
|
|
142
|
+
idType?: BetterAuthIdType;
|
|
118
143
|
/**
|
|
119
144
|
* Mongoose schema indexes to attach to the overlay model.
|
|
120
145
|
*
|
|
@@ -200,6 +225,40 @@ declare function createBetterAuthOverlay<TDoc = Record<string, unknown>>(options
|
|
|
200
225
|
* `Model` — so the host passes whichever it has, and mongokit keeps mongoose +
|
|
201
226
|
* the mongodb driver as peer deps with zero runtime import.
|
|
202
227
|
*/
|
|
228
|
+
/**
|
|
229
|
+
* Ensure the TTL index that reaps expired Better Auth sessions.
|
|
230
|
+
*
|
|
231
|
+
* BA stamps `expiresAt` on every session (tunable via `session.expiresIn`,
|
|
232
|
+
* default 7 days) but its mongo adapter creates NO index for it, and nothing
|
|
233
|
+
* else sweeps the collection — so `session` grows without bound on the hottest
|
|
234
|
+
* auth table. Measured on two live deployments before this existed: 568 rows
|
|
235
|
+
* (233 expired) and 909 rows of which EVERY ONE was expired, the oldest five
|
|
236
|
+
* months old.
|
|
237
|
+
*
|
|
238
|
+
* It lives here rather than in a host or a spine module because this kit is
|
|
239
|
+
* what knows Better Auth's collection names — a host-side fix only protects
|
|
240
|
+
* that host, and every other consumer keeps leaking.
|
|
241
|
+
*
|
|
242
|
+
* `expireAfterSeconds: 0` means "delete once `expiresAt` has passed", so
|
|
243
|
+
* mongo's background task keeps the collection bounded with no application
|
|
244
|
+
* code. Idempotent — re-creating an existing index is a no-op.
|
|
245
|
+
*
|
|
246
|
+
* NOT called automatically by {@link registerBetterAuthStubs}: that function is
|
|
247
|
+
* synchronous and writes nothing to the database, and silently issuing DDL from
|
|
248
|
+
* a model-registration helper would be a surprise. Call this once at boot.
|
|
249
|
+
*
|
|
250
|
+
* @returns `true` when an index was created or already present, `false` when
|
|
251
|
+
* the attempt failed (logged by the caller, never thrown — a missing
|
|
252
|
+
* TTL degrades storage, and refusing to boot over it is worse).
|
|
253
|
+
*/
|
|
254
|
+
declare function ensureBetterAuthSessionTtl(connection: {
|
|
255
|
+
collection(name: string): {
|
|
256
|
+
createIndex(keys: Record<string, 1>, opts: Record<string, unknown>): Promise<unknown>;
|
|
257
|
+
};
|
|
258
|
+
}, options?: {
|
|
259
|
+
collection?: string;
|
|
260
|
+
indexName?: string;
|
|
261
|
+
}): Promise<boolean>;
|
|
203
262
|
interface SessionUpdaterLike {
|
|
204
263
|
updateMany(filter: Record<string, unknown>, update: Record<string, unknown>): Promise<{
|
|
205
264
|
modifiedCount?: number;
|
|
@@ -234,4 +293,4 @@ declare function clearActiveOrganizationFromSessions(sessions: SessionUpdaterLik
|
|
|
234
293
|
userId?: unknown;
|
|
235
294
|
}): Promise<number>;
|
|
236
295
|
//#endregion
|
|
237
|
-
export { BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, registerBetterAuthStubs };
|
|
296
|
+
export { BetterAuthIdType, BetterAuthInstance, BetterAuthOverlayOptions, type BetterAuthPluginKey, MongooseLike, RegisterBetterAuthStubsOptions, SessionUpdaterLike, clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as Repository } from "../Repository-BArvU32x.mjs";
|
|
2
|
-
import { n as createMongooseAdapter } from "../adapter-
|
|
2
|
+
import { n as createMongooseAdapter } from "../adapter-DlaOGcQN.mjs";
|
|
3
3
|
import { asReadOnlyRepo } from "@classytic/repo-core/repository";
|
|
4
4
|
import { pluralizeBetterAuthCollection, resolveBetterAuthCollections } from "@classytic/repo-core/better-auth";
|
|
5
5
|
//#region src/better-auth/index.ts
|
|
@@ -40,6 +40,10 @@ function missingIndexesOn(existing, declared) {
|
|
|
40
40
|
const existingIndexes = (existing?.schema)?.indexes?.() ?? [];
|
|
41
41
|
return declared.filter((d) => !existingIndexes.some(([fields, options]) => indexFieldsKey(fields) === indexFieldsKey(d.fields) && declaredOptionsSatisfied(d.options ?? {}, options ?? {})));
|
|
42
42
|
}
|
|
43
|
+
/** The `_id` path for a stub/overlay schema, or `{}` to inherit mongoose's ObjectId default. */
|
|
44
|
+
function idPath(idType) {
|
|
45
|
+
return idType === "string" ? { _id: { type: String } } : {};
|
|
46
|
+
}
|
|
43
47
|
/**
|
|
44
48
|
* Register stub Mongoose models for Better Auth's collections so that
|
|
45
49
|
* Mongoose-based resources can `.populate()` references to BA-owned
|
|
@@ -68,7 +72,7 @@ function registerBetterAuthStubs(mongoose, options = {}) {
|
|
|
68
72
|
const registered = [];
|
|
69
73
|
for (const finalName of names) {
|
|
70
74
|
if (finalName in mongoose.models) continue;
|
|
71
|
-
const schema = new mongoose.Schema(
|
|
75
|
+
const schema = new mongoose.Schema(idPath(options.idType), {
|
|
72
76
|
strict: false,
|
|
73
77
|
collection: finalName,
|
|
74
78
|
timestamps: false
|
|
@@ -135,7 +139,10 @@ async function createBetterAuthOverlay(options) {
|
|
|
135
139
|
}
|
|
136
140
|
let Model = mongoose.models[finalName];
|
|
137
141
|
if (!Model) {
|
|
138
|
-
const schema = new mongoose.Schema(
|
|
142
|
+
const schema = new mongoose.Schema({
|
|
143
|
+
...idPath(options.idType),
|
|
144
|
+
...additionalFields
|
|
145
|
+
}, {
|
|
139
146
|
strict: false,
|
|
140
147
|
collection: finalName,
|
|
141
148
|
timestamps: false
|
|
@@ -152,6 +159,50 @@ async function createBetterAuthOverlay(options) {
|
|
|
152
159
|
});
|
|
153
160
|
}
|
|
154
161
|
/**
|
|
162
|
+
* Minimal structural surface for the one write we need. Satisfied by BOTH a
|
|
163
|
+
* native MongoDB `Collection` (`db.collection('session')`) and a Mongoose
|
|
164
|
+
* `Model` — so the host passes whichever it has, and mongokit keeps mongoose +
|
|
165
|
+
* the mongodb driver as peer deps with zero runtime import.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* Ensure the TTL index that reaps expired Better Auth sessions.
|
|
169
|
+
*
|
|
170
|
+
* BA stamps `expiresAt` on every session (tunable via `session.expiresIn`,
|
|
171
|
+
* default 7 days) but its mongo adapter creates NO index for it, and nothing
|
|
172
|
+
* else sweeps the collection — so `session` grows without bound on the hottest
|
|
173
|
+
* auth table. Measured on two live deployments before this existed: 568 rows
|
|
174
|
+
* (233 expired) and 909 rows of which EVERY ONE was expired, the oldest five
|
|
175
|
+
* months old.
|
|
176
|
+
*
|
|
177
|
+
* It lives here rather than in a host or a spine module because this kit is
|
|
178
|
+
* what knows Better Auth's collection names — a host-side fix only protects
|
|
179
|
+
* that host, and every other consumer keeps leaking.
|
|
180
|
+
*
|
|
181
|
+
* `expireAfterSeconds: 0` means "delete once `expiresAt` has passed", so
|
|
182
|
+
* mongo's background task keeps the collection bounded with no application
|
|
183
|
+
* code. Idempotent — re-creating an existing index is a no-op.
|
|
184
|
+
*
|
|
185
|
+
* NOT called automatically by {@link registerBetterAuthStubs}: that function is
|
|
186
|
+
* synchronous and writes nothing to the database, and silently issuing DDL from
|
|
187
|
+
* a model-registration helper would be a surprise. Call this once at boot.
|
|
188
|
+
*
|
|
189
|
+
* @returns `true` when an index was created or already present, `false` when
|
|
190
|
+
* the attempt failed (logged by the caller, never thrown — a missing
|
|
191
|
+
* TTL degrades storage, and refusing to boot over it is worse).
|
|
192
|
+
*/
|
|
193
|
+
async function ensureBetterAuthSessionTtl(connection, options = {}) {
|
|
194
|
+
const name = options.collection ?? "session";
|
|
195
|
+
try {
|
|
196
|
+
await connection.collection(name).createIndex({ expiresAt: 1 }, {
|
|
197
|
+
expireAfterSeconds: 0,
|
|
198
|
+
name: options.indexName ?? "ttl_session_expiresAt"
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
155
206
|
* Clear a dangling `activeOrganizationId` from Better Auth `session` rows.
|
|
156
207
|
*
|
|
157
208
|
* WHY THIS EXISTS: Better Auth purges its `member` / `invitation` rows when an
|
|
@@ -182,4 +233,4 @@ async function clearActiveOrganizationFromSessions(sessions, organizationId, opt
|
|
|
182
233
|
return (await sessions.updateMany(filter, { $set: { activeOrganizationId: null } })).modifiedCount ?? 0;
|
|
183
234
|
}
|
|
184
235
|
//#endregion
|
|
185
|
-
export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, registerBetterAuthStubs };
|
|
236
|
+
export { clearActiveOrganizationFromSessions, createBetterAuthOverlay, ensureBetterAuthSessionTtl, registerBetterAuthStubs };
|
package/dist/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForU
|
|
|
15
15
|
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
16
16
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
17
17
|
import { n as createMemoryCache, t as toPlain } from "./to-plain-vksRrLtc.mjs";
|
|
18
|
-
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-
|
|
18
|
+
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-waUVQwK4.mjs";
|
|
19
19
|
import { runChunkedPurge } from "@classytic/repo-core/repository";
|
|
20
20
|
import { isControlParam } from "@classytic/repo-core/query-parser";
|
|
21
21
|
//#region src/actions/cascade-references.ts
|
|
@@ -474,9 +474,37 @@ function stripDefaultsDeep(node) {
|
|
|
474
474
|
delete obj.default;
|
|
475
475
|
for (const value of Object.values(obj)) stripDefaultsDeep(value);
|
|
476
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Recursively delete every `required` keyword from a JSON-schema subtree
|
|
479
|
+
* (properties, array items, nested subdocuments).
|
|
480
|
+
*
|
|
481
|
+
* PATCH semantics mean every field is optional at EVERY nesting depth, not
|
|
482
|
+
* just the root — a caller may send a partial subdocument-array item (e.g.
|
|
483
|
+
* `variants: [{ sku, isActive }]` to toggle a flag) without re-supplying
|
|
484
|
+
* every field the CREATE schema requires for that item. The one-line
|
|
485
|
+
* `delete clone.required` below only stripped the top-level key; a
|
|
486
|
+
* DocumentArray's `items` schema carries its OWN `required` (built by
|
|
487
|
+
* `subSchemaToJsonSchema`), which survived untouched — so a partial nested
|
|
488
|
+
* item still failed AJV's `required` check before the request ever reached
|
|
489
|
+
* the repository (real defect: PATCHing a product to add ONE variant with
|
|
490
|
+
* no `variationAttributes` 400'd on `variants/0` missing `attributes`, even
|
|
491
|
+
* though `attributes: {}` is a valid, already-persisted value). Same fix
|
|
492
|
+
* shape as `stripDefaultsDeep` above, for the identical reason: an UPDATE
|
|
493
|
+
* schema's partial-field semantics belong to the whole tree, not its root.
|
|
494
|
+
*/
|
|
495
|
+
function stripRequiredDeep(node) {
|
|
496
|
+
if (Array.isArray(node)) {
|
|
497
|
+
for (const item of node) stripRequiredDeep(item);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (!node || typeof node !== "object") return;
|
|
501
|
+
const obj = node;
|
|
502
|
+
delete obj.required;
|
|
503
|
+
for (const value of Object.values(obj)) stripRequiredDeep(value);
|
|
504
|
+
}
|
|
477
505
|
function buildJsonSchemaForUpdate(createJson, options) {
|
|
478
506
|
const clone = JSON.parse(JSON.stringify(createJson));
|
|
479
|
-
|
|
507
|
+
stripRequiredDeep(clone);
|
|
480
508
|
const fieldsToOmit = collectFieldsToOmit(options, "update");
|
|
481
509
|
applyFieldRules(clone, fieldsToOmit, options);
|
|
482
510
|
stripDefaultsDeep(clone);
|
package/dist/utils/index.mjs
CHANGED
|
@@ -4,5 +4,5 @@ import { t as configureLogger } from "../logger-K7dPmu2U.mjs";
|
|
|
4
4
|
import { t as createError } from "../error-BuByZwKU.mjs";
|
|
5
5
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "../field-selection-FSQFLpsU.mjs";
|
|
6
6
|
import { n as createMemoryCache, t as toPlain } from "../to-plain-vksRrLtc.mjs";
|
|
7
|
-
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-
|
|
7
|
+
import { n as buildCrudSchemasFromMongooseSchema, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-waUVQwK4.mjs";
|
|
8
8
|
export { DEFAULT_ID_CHUNK, buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, collectIds, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getMongooseProjection, getSchemaIdType, idChunks, isObjectIdInstance, isValidIdForType, keysetFilter, narrowToIds, selectKeysetChunk, toObjectId, toPlain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/mongokit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
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,
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
},
|
|
123
123
|
"homepage": "https://github.com/classytic/mongokit#readme",
|
|
124
124
|
"peerDependencies": {
|
|
125
|
-
"@classytic/repo-core": ">=0.
|
|
125
|
+
"@classytic/repo-core": ">=0.25.0",
|
|
126
126
|
"mongodb-memory-server": ">=10.4.3",
|
|
127
127
|
"mongoose": ">=9.4.1"
|
|
128
128
|
},
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"@better-auth/mongo-adapter": "^1.6.29",
|
|
175
175
|
"@biomejs/biome": "^2.5.8",
|
|
176
176
|
"@classytic/dev-tools": "^0.2.0",
|
|
177
|
-
"@classytic/repo-core": "^0.
|
|
177
|
+
"@classytic/repo-core": "^0.25.0",
|
|
178
178
|
"@types/node": "^26.2.0",
|
|
179
179
|
"@vitest/coverage-v8": "^4.1.10",
|
|
180
180
|
"ajv": "^8.20.0",
|