@better-auth/drizzle-adapter 1.7.1 → 1.7.3

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/index.mjs CHANGED
@@ -1,8 +1,38 @@
1
- import { a as insensitiveNe, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, r as insensitiveIlike } from "./query-builders-D2eE7gbx.mjs";
1
+ import { a as insensitiveNe, c as getOneToOneRelationKey, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, r as insensitiveIlike, s as buildRelationKeysByModel } from "./query-builders-CBLMSM7v.mjs";
2
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
+ import { checksSchema, createSchemaCheck, diffSchema, getExpectedSchema, registerSchemaCheck } from "@better-auth/core/db/internal";
3
4
  import { logger } from "@better-auth/core/env";
4
5
  import { BetterAuthError } from "@better-auth/core/error";
5
- import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
6
+ import { Column, Table, and, asc, count, desc, eq, getTableColumns, gt, gte, inArray, is, isNotNull, isNull, like, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
7
+ //#region src/schema-check.ts
8
+ /**
9
+ * Reads a Drizzle schema object the way the adapter addresses it: each table
10
+ * by the key it is exported under, each column by its property name. Values
11
+ * that are not tables, such as relations, are skipped.
12
+ */
13
+ function introspectDrizzleSchema(schema) {
14
+ const tables = [];
15
+ for (const [name, table] of Object.entries(schema)) {
16
+ if (!is(table, Table)) continue;
17
+ const columns = Object.entries(getTableColumns(table)).map(([key, column]) => ({
18
+ name: key,
19
+ nullable: !column.notNull,
20
+ hasDefault: column.hasDefault || column.generated !== void 0 || column.generatedIdentity !== void 0
21
+ }));
22
+ tables.push({
23
+ name,
24
+ columns
25
+ });
26
+ }
27
+ return tables;
28
+ }
29
+ /**
30
+ * Compares a Drizzle schema object with the tables this configuration writes.
31
+ */
32
+ function findDrizzleSchemaProblems(schema, options, usePlural) {
33
+ return diffSchema(getExpectedSchema(options, { usePlural }), introspectDrizzleSchema(schema));
34
+ }
35
+ //#endregion
6
36
  //#region src/drizzle-adapter.ts
7
37
  /**
8
38
  * Derive the number of affected rows from a Drizzle write result.
@@ -48,6 +78,7 @@ function hasDriverRowCount(result) {
48
78
  const drizzleAdapter = (db, config) => {
49
79
  let lazyOptions = null;
50
80
  let mysqlNoIdWarned = false;
81
+ const relationKeysByModel = buildRelationKeysByModel(db._?.schema);
51
82
  const createCustomAdapter = (db, inTransaction = false) => ({ getFieldName, getDefaultFieldName, getDefaultModelName, options, schema: baSchema }) => {
52
83
  if (config.provider === "mysql" && options.advanced?.database?.generateId === false && !mysqlNoIdWarned) {
53
84
  mysqlNoIdWarned = true;
@@ -115,15 +146,19 @@ const drizzleAdapter = (db, config) => {
115
146
  };
116
147
  function convertWhereClause(where, model) {
117
148
  const schemaModel = getSchema(model);
149
+ const resolveFieldName = (where) => {
150
+ const field = getFieldName({
151
+ model,
152
+ field: where.field
153
+ });
154
+ if (!is(schemaModel[field], Column)) throw new BetterAuthError(`The field "${where.field}" does not exist in the schema for the model "${model}". Please update your schema.`);
155
+ return field;
156
+ };
118
157
  if (!where) return [];
119
158
  if (where.length === 1) {
120
159
  const w = where[0];
121
160
  if (!w) return [];
122
- const field = getFieldName({
123
- model,
124
- field: w.field
125
- });
126
- if (!schemaModel[field]) throw new BetterAuthError(`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`);
161
+ const field = resolveFieldName(w);
127
162
  const isInsensitive = (w.mode ?? "sensitive") === "insensitive" && (typeof w.value === "string" || Array.isArray(w.value) && w.value.every((v) => typeof v === "string"));
128
163
  if (w.operator === "in") {
129
164
  if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`);
@@ -163,10 +198,7 @@ const drizzleAdapter = (db, config) => {
163
198
  const andGroup = where.filter((w) => w.connector === "AND" || !w.connector);
164
199
  const orGroup = where.filter((w) => w.connector === "OR");
165
200
  const andClause = and(...andGroup.map((w) => {
166
- const field = getFieldName({
167
- model,
168
- field: w.field
169
- });
201
+ const field = resolveFieldName(w);
170
202
  const isInsensitive = (w.mode ?? "sensitive") === "insensitive" && (typeof w.value === "string" || Array.isArray(w.value) && w.value.every((v) => typeof v === "string"));
171
203
  if (w.operator === "in") {
172
204
  if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`);
@@ -204,11 +236,7 @@ const drizzleAdapter = (db, config) => {
204
236
  return eq(schemaModel[field], w.value);
205
237
  }));
206
238
  const orClause = or(...orGroup.map((w) => {
207
- const field = getFieldName({
208
- model,
209
- field: w.field
210
- });
211
- if (!schemaModel[field]) throw new BetterAuthError(`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`);
239
+ const field = resolveFieldName(w);
212
240
  const isInsensitive = (w.mode ?? "sensitive") === "insensitive" && (typeof w.value === "string" || Array.isArray(w.value) && w.value.every((v) => typeof v === "string"));
213
241
  if (w.operator === "in") {
214
242
  if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`);
@@ -295,6 +323,16 @@ const drizzleAdapter = (db, config) => {
295
323
  }
296
324
  return null;
297
325
  }
326
+ function getJoinRelationKey(baseModel, joinModel, relationKeys, isUnique) {
327
+ if (isUnique) return getOneToOneRelationKey({
328
+ baseModel,
329
+ joinModel,
330
+ relationKeys,
331
+ schema: baSchema,
332
+ getDefaultModelName
333
+ });
334
+ return config.usePlural ? joinModel : `${joinModel}s`;
335
+ }
298
336
  return {
299
337
  async create({ model, data: values }) {
300
338
  const schemaModel = getSchema(model);
@@ -311,15 +349,19 @@ const drizzleAdapter = (db, config) => {
311
349
  logger.info("Falling back to regular query");
312
350
  } else {
313
351
  let includes;
314
- const pluralJoinResults = [];
352
+ const renamedJoinResults = [];
353
+ const relationKeys = relationKeysByModel.get(queryModel);
315
354
  includes = {};
316
355
  const joinEntries = Object.entries(join);
317
- for (const [model, joinAttr] of joinEntries) {
356
+ for (const [joinModel, joinAttr] of joinEntries) {
318
357
  const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
319
358
  const isUnique = joinAttr.relation === "one-to-one";
320
- const pluralSuffix = isUnique || config.usePlural ? "" : "s";
321
- includes[`${model}${pluralSuffix}`] = isUnique ? true : { limit };
322
- if (!isUnique) pluralJoinResults.push(`${model}${pluralSuffix}`);
359
+ const relationKey = getJoinRelationKey(model, joinModel, relationKeys, isUnique);
360
+ includes[relationKey] = isUnique ? true : { limit };
361
+ if (relationKey !== joinModel) renamedJoinResults.push({
362
+ key: relationKey,
363
+ target: joinModel
364
+ });
323
365
  }
324
366
  const res = await db.query[queryModel].findFirst({
325
367
  where: clause[0],
@@ -332,10 +374,9 @@ const drizzleAdapter = (db, config) => {
332
374
  }, {}) : void 0,
333
375
  with: includes
334
376
  });
335
- if (res) for (const pluralJoinResult of pluralJoinResults) {
336
- const singularKey = !config.usePlural ? pluralJoinResult.slice(0, -1) : pluralJoinResult;
337
- res[singularKey] = res[pluralJoinResult];
338
- if (pluralJoinResult !== singularKey) delete res[pluralJoinResult];
377
+ if (res) for (const { key, target } of renamedJoinResults) {
378
+ res[target] = res[key];
379
+ delete res[key];
339
380
  }
340
381
  return res;
341
382
  }
@@ -364,15 +405,19 @@ const drizzleAdapter = (db, config) => {
364
405
  logger.info("Falling back to regular query");
365
406
  } else {
366
407
  let includes;
367
- const pluralJoinResults = [];
408
+ const renamedJoinResults = [];
409
+ const relationKeys = relationKeysByModel.get(queryModel);
368
410
  includes = {};
369
411
  const joinEntries = Object.entries(join);
370
- for (const [model, joinAttr] of joinEntries) {
412
+ for (const [joinModel, joinAttr] of joinEntries) {
371
413
  const isUnique = joinAttr.relation === "one-to-one";
372
414
  const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
373
- const pluralSuffix = isUnique || config.usePlural ? "" : "s";
374
- includes[`${model}${pluralSuffix}`] = isUnique ? true : { limit };
375
- if (!isUnique) pluralJoinResults.push(`${model}${pluralSuffix}`);
415
+ const relationKey = getJoinRelationKey(model, joinModel, relationKeys, isUnique);
416
+ includes[relationKey] = isUnique ? true : { limit };
417
+ if (relationKey !== joinModel) renamedJoinResults.push({
418
+ key: relationKey,
419
+ target: joinModel
420
+ });
376
421
  }
377
422
  let orderBy = void 0;
378
423
  if (sortBy?.field) orderBy = [sortFn(schemaModel[getFieldName({
@@ -393,11 +438,9 @@ const drizzleAdapter = (db, config) => {
393
438
  offset: offset ?? 0,
394
439
  orderBy
395
440
  });
396
- if (res) for (const item of res) for (const pluralJoinResult of pluralJoinResults) {
397
- const singularKey = !config.usePlural ? pluralJoinResult.slice(0, -1) : pluralJoinResult;
398
- if (singularKey === pluralJoinResult) continue;
399
- item[singularKey] = item[pluralJoinResult];
400
- delete item[pluralJoinResult];
441
+ if (res) for (const item of res) for (const { key, target } of renamedJoinResults) {
442
+ item[target] = item[key];
443
+ delete item[key];
401
444
  }
402
445
  return res;
403
446
  }
@@ -554,7 +597,9 @@ const drizzleAdapter = (db, config) => {
554
597
  const adapter = createAdapterFactory(adapterOptions);
555
598
  return (options) => {
556
599
  lazyOptions = options;
557
- return adapter(options);
600
+ const instance = adapter(options);
601
+ if (checksSchema(options)) registerSchemaCheck(instance, createSchemaCheck(async () => findDrizzleSchemaProblems(config.schema ?? db._?.fullSchema ?? {}, options, config.usePlural), "drizzle"));
602
+ return instance;
558
603
  };
559
604
  };
560
605
  //#endregion
@@ -1,4 +1,30 @@
1
1
  import { ilike, sql } from "drizzle-orm";
2
+ //#region src/join-relation-key.ts
3
+ /**
4
+ * Reads the relation registry used to build Drizzle relational queries.
5
+ *
6
+ * - Drizzle 0.x (Relations v1): `db._.schema`
7
+ * - Drizzle 1.x (Relations v2): `db._.relations`
8
+ */
9
+ function buildRelationKeysByModel(relationRegistry) {
10
+ const relationKeysByModel = /* @__PURE__ */ new Map();
11
+ for (const [model, tableMetadata] of Object.entries(relationRegistry ?? {})) {
12
+ if (!tableMetadata.relations) continue;
13
+ relationKeysByModel.set(model, new Set(Object.keys(tableMetadata.relations)));
14
+ }
15
+ return relationKeysByModel;
16
+ }
17
+ function getOneToOneRelationKey({ baseModel, joinModel, relationKeys, schema, getDefaultModelName }) {
18
+ const defaultBaseModelName = getDefaultModelName(baseModel);
19
+ const defaultJoinModelName = getDefaultModelName(joinModel);
20
+ const joinModelFields = schema[defaultJoinModelName]?.fields ?? {};
21
+ const generatedRelationKey = Object.values(joinModelFields).some((field) => field.references && getDefaultModelName(field.references.model) === defaultBaseModelName) ? joinModel : schema[defaultJoinModelName]?.modelName ?? defaultJoinModelName;
22
+ if (!relationKeys?.size) return joinModel;
23
+ if (relationKeys.has(generatedRelationKey)) return generatedRelationKey;
24
+ if (relationKeys.has(joinModel)) return joinModel;
25
+ return generatedRelationKey;
26
+ }
27
+ //#endregion
2
28
  //#region src/query-builders.ts
3
29
  /**
4
30
  * Case-insensitive LIKE/ILIKE for pattern matching.
@@ -49,4 +75,4 @@ function insensitiveNe(column, value) {
49
75
  return sql`LOWER(${column}) <> LOWER(${value})`;
50
76
  }
51
77
  //#endregion
52
- export { insensitiveNe as a, insensitiveInArray as i, insensitiveEq as n, insensitiveNotInArray as o, insensitiveIlike as r, escapedLike as t };
78
+ export { insensitiveNe as a, getOneToOneRelationKey as c, insensitiveInArray as i, insensitiveEq as n, insensitiveNotInArray as o, insensitiveIlike as r, buildRelationKeysByModel as s, escapedLike as t };
@@ -1,4 +1,4 @@
1
- import { a as insensitiveNe, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, t as escapedLike } from "../query-builders-D2eE7gbx.mjs";
1
+ import { a as insensitiveNe, c as getOneToOneRelationKey, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, s as buildRelationKeysByModel, t as escapedLike } from "../query-builders-CBLMSM7v.mjs";
2
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
3
  import { logger } from "@better-auth/core/env";
4
4
  import { BetterAuthError } from "@better-auth/core/error";
@@ -72,15 +72,14 @@ function applyWhereOperator(column, w, fieldLabel, provider) {
72
72
  const drizzleAdapter = (db, config) => {
73
73
  let lazyOptions = null;
74
74
  let mysqlNoIdWarned = false;
75
+ const relationKeysByModel = buildRelationKeysByModel(db._?.relations);
75
76
  const createCustomAdapter = (db, inTransaction = false) => ({ getFieldName, getDefaultModelName, options, schema: baSchema }) => {
76
77
  if (config.provider === "mysql" && options.advanced?.database?.generateId === false && !mysqlNoIdWarned) {
77
78
  mysqlNoIdWarned = true;
78
79
  logger.warn("[Drizzle Adapter] MySQL does not support INSERT...RETURNING. With generateId set to false, the adapter uses best-effort fallback strategies (unique columns, full-field match) to retrieve inserted rows. For reliable behavior, use Better Auth's default ID generation, a custom generateId function, or generateId: \"serial\" for auto-increment.");
79
80
  }
80
81
  function getSchema(model) {
81
- const schema = config.schema || db._.fullSchema;
82
- if (!schema) throw new BetterAuthError("Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.");
83
- const schemaModel = schema[model];
82
+ const schemaModel = config.schema?.[model] ?? db._?.relations?.[model]?.table ?? db._?.fullSchema?.[model];
84
83
  if (!schemaModel) throw new BetterAuthError(`[# Drizzle Adapter]: The model "${model}" was not found in the schema object. Please pass the schema directly to the adapter options.`);
85
84
  return schemaModel;
86
85
  }
@@ -109,14 +108,16 @@ const drizzleAdapter = (db, config) => {
109
108
  }
110
109
  return null;
111
110
  }
112
- /**
113
- * Mirror the schema generator's relation-key naming. One-to-one keeps
114
- * the singular model name. One-to-many is pluralized unless the model
115
- * already ends in "s" or `usePlural` keeps the schema keys as-is.
116
- */
117
- function getJoinRelationKey(model, isUnique) {
118
- if (isUnique || config.usePlural || model.endsWith("s")) return model;
119
- return `${model}s`;
111
+ function getJoinRelationKey(baseModel, joinModel, relationKeys, isUnique) {
112
+ if (isUnique) return getOneToOneRelationKey({
113
+ baseModel,
114
+ joinModel,
115
+ relationKeys,
116
+ schema: baSchema,
117
+ getDefaultModelName
118
+ });
119
+ if (config.usePlural || joinModel.endsWith("s")) return joinModel;
120
+ return `${joinModel}s`;
120
121
  }
121
122
  const withReturning = async (model, builder, data, where) => {
122
123
  if (config.provider !== "mysql") return (await builder.returning())[0];
@@ -290,17 +291,18 @@ const drizzleAdapter = (db, config) => {
290
291
  logger.info("Falling back to regular query");
291
292
  } else {
292
293
  let includes;
293
- const pluralJoinResults = [];
294
+ const renamedJoinResults = [];
295
+ const relationKeys = relationKeysByModel.get(queryModel);
294
296
  includes = {};
295
297
  const joinEntries = Object.entries(join);
296
- for (const [model, joinAttr] of joinEntries) {
298
+ for (const [joinModel, joinAttr] of joinEntries) {
297
299
  const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
298
300
  const isUnique = joinAttr.relation === "one-to-one";
299
- const relationKey = getJoinRelationKey(model, isUnique);
301
+ const relationKey = getJoinRelationKey(model, joinModel, relationKeys, isUnique);
300
302
  includes[relationKey] = isUnique ? true : { limit };
301
- if (!isUnique) pluralJoinResults.push({
303
+ if (relationKey !== joinModel) renamedJoinResults.push({
302
304
  key: relationKey,
303
- target: model
305
+ target: joinModel
304
306
  });
305
307
  }
306
308
  const clause = convertNewWhereClause(where, model);
@@ -315,8 +317,7 @@ const drizzleAdapter = (db, config) => {
315
317
  }, {}) : void 0,
316
318
  with: includes
317
319
  });
318
- if (res) for (const { key, target } of pluralJoinResults) {
319
- if (key === target) continue;
320
+ if (res) for (const { key, target } of renamedJoinResults) {
320
321
  res[target] = res[key];
321
322
  delete res[key];
322
323
  }
@@ -347,17 +348,18 @@ const drizzleAdapter = (db, config) => {
347
348
  logger.info("Falling back to regular query");
348
349
  } else {
349
350
  let includes;
350
- const pluralJoinResults = [];
351
+ const renamedJoinResults = [];
352
+ const relationKeys = relationKeysByModel.get(queryModel);
351
353
  includes = {};
352
354
  const joinEntries = Object.entries(join);
353
- for (const [model, joinAttr] of joinEntries) {
355
+ for (const [joinModel, joinAttr] of joinEntries) {
354
356
  const isUnique = joinAttr.relation === "one-to-one";
355
357
  const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
356
- const relationKey = getJoinRelationKey(model, isUnique);
358
+ const relationKey = getJoinRelationKey(model, joinModel, relationKeys, isUnique);
357
359
  includes[relationKey] = isUnique ? true : { limit };
358
- if (!isUnique) pluralJoinResults.push({
360
+ if (relationKey !== joinModel) renamedJoinResults.push({
359
361
  key: relationKey,
360
- target: model
362
+ target: joinModel
361
363
  });
362
364
  }
363
365
  let orderBy = void 0;
@@ -379,8 +381,7 @@ const drizzleAdapter = (db, config) => {
379
381
  offset: offset ?? 0,
380
382
  orderBy
381
383
  });
382
- if (res) for (const item of res) for (const { key, target } of pluralJoinResults) {
383
- if (key === target) continue;
384
+ if (res) for (const item of res) for (const { key, target } of renamedJoinResults) {
384
385
  item[target] = item[key];
385
386
  delete item[key];
386
387
  }
@@ -505,7 +506,7 @@ const drizzleAdapter = (db, config) => {
505
506
  return (await db.update(schemaModel).set(assignments).where(inArray(idColumn, targetIds)).returning())[0] ?? null;
506
507
  },
507
508
  async createSchema(props) {
508
- const { generateDrizzleSchema } = await import("../generate-drizzle-schema-huQqmolx.mjs");
509
+ const { generateDrizzleSchema } = await import("../generate-drizzle-schema-iWvrXnu0.mjs");
509
510
  return await generateDrizzleSchema({
510
511
  adapterConfig: config,
511
512
  options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/drizzle-adapter",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "bugs": {
5
5
  "url": "https://github.com/better-auth/better-auth/issues"
6
6
  },
@@ -45,7 +45,7 @@
45
45
  "peerDependencies": {
46
46
  "@better-auth/utils": "0.4.2",
47
47
  "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
48
- "@better-auth/core": "^1.7.1"
48
+ "@better-auth/core": "^1.7.3"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "drizzle-orm": {
@@ -57,7 +57,7 @@
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "tsdown": "0.21.10",
59
59
  "typescript": "^6.0.3",
60
- "@better-auth/core": "1.7.1"
60
+ "@better-auth/core": "1.7.3"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "tsdown",
@@ -1,8 +1,8 @@
1
1
  import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
2
+ import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
2
3
  import { existsSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { getAuthTables } from "@better-auth/core/db";
5
- import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
6
6
  //#region src/relations-v2/generate-drizzle-schema.ts
7
7
  function convertToSnakeCase(str, camelCase) {
8
8
  if (camelCase) return str;