@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.
@@ -1,2 +1,2 @@
1
- import { n as createMongooseAdapter, r as isMongooseModel, t as MongooseAdapter } from "../adapter-BitHg-CN.mjs";
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-DKbnKduN.mjs";
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-BitHg-CN.mjs";
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(additionalFields, {
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-DKbnKduN.mjs";
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
- delete clone.required;
507
+ stripRequiredDeep(clone);
480
508
  const fieldsToOmit = collectFieldsToOmit(options, "update");
481
509
  applyFieldRules(clone, fieldsToOmit, options);
482
510
  stripDefaultsDeep(clone);
@@ -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-DKbnKduN.mjs";
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.33.0",
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.23.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.23.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",