@better-auth/drizzle-adapter 1.7.0-beta.1 → 1.7.0-beta.10

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.
@@ -0,0 +1,311 @@
1
+ import { initGetFieldName, initGetModelName } from "@better-auth/core/db/adapter";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getAuthTables } from "@better-auth/core/db";
5
+ //#region src/relations-v2/generate-drizzle-schema.ts
6
+ function convertToSnakeCase(str, camelCase) {
7
+ if (camelCase) return str;
8
+ return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
9
+ }
10
+ const generateDrizzleSchema = async ({ options, file, provider, adapterConfig, camelCase, tables: propsTables }) => {
11
+ const tables = propsTables ?? getAuthTables(options);
12
+ const filePath = file || "./auth-schema.ts";
13
+ const databaseType = provider;
14
+ if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
15
+ const fileExist = existsSync(filePath);
16
+ let code = generateImport({
17
+ databaseType,
18
+ tables,
19
+ options
20
+ });
21
+ const getModelName = initGetModelName({
22
+ schema: tables,
23
+ usePlural: adapterConfig?.usePlural
24
+ });
25
+ const getSingularModelName = initGetModelName({
26
+ schema: tables,
27
+ usePlural: false
28
+ });
29
+ const getFieldName = initGetFieldName({
30
+ schema: tables,
31
+ usePlural: adapterConfig?.usePlural
32
+ });
33
+ for (const tableKey in tables) {
34
+ const table = tables[tableKey];
35
+ const modelName = getModelName(tableKey);
36
+ const fields = table.fields;
37
+ function getType(name, field) {
38
+ if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
39
+ name = convertToSnakeCase(name, camelCase);
40
+ if (field.references?.field === "id") {
41
+ const useNumberId = options.advanced?.database?.generateId === "serial";
42
+ const useUUIDs = options.advanced?.database?.generateId === "uuid";
43
+ if (useNumberId) if (databaseType === "pg") return `integer('${name}')`;
44
+ else if (databaseType === "mysql") return `int('${name}')`;
45
+ else return `integer('${name}')`;
46
+ if (useUUIDs && databaseType === "pg") return `uuid('${name}')`;
47
+ if (field.references.field) {
48
+ if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`;
49
+ }
50
+ return `text('${name}')`;
51
+ }
52
+ const type = field.type;
53
+ if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
54
+ sqlite: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
55
+ pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
56
+ mysql: `mysqlEnum('${name}', [${type.map((x) => `'${x}'`).join(", ")}])`
57
+ }[databaseType];
58
+ else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
59
+ const dbTypeMap = {
60
+ string: {
61
+ sqlite: `text('${name}')`,
62
+ pg: `text('${name}')`,
63
+ mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
64
+ },
65
+ boolean: {
66
+ sqlite: `integer('${name}', { mode: 'boolean' })`,
67
+ pg: `boolean('${name}')`,
68
+ mysql: `boolean('${name}')`
69
+ },
70
+ number: {
71
+ sqlite: `integer('${name}')`,
72
+ pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
73
+ mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
74
+ },
75
+ date: {
76
+ sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
77
+ pg: `timestamp('${name}')`,
78
+ mysql: `timestamp('${name}', { fsp: 3 })`
79
+ },
80
+ "number[]": {
81
+ sqlite: `text('${name}', { mode: "json" })`,
82
+ pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
83
+ mysql: `json('${name}')`
84
+ },
85
+ "string[]": {
86
+ sqlite: `text('${name}', { mode: "json" })`,
87
+ pg: `text('${name}').array()`,
88
+ mysql: `json('${name}')`
89
+ },
90
+ json: {
91
+ sqlite: `text('${name}', { mode: "json" })`,
92
+ pg: `jsonb('${name}')`,
93
+ mysql: `json('${name}')`
94
+ }
95
+ }[type];
96
+ if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
97
+ return dbTypeMap[databaseType];
98
+ }
99
+ let id = "";
100
+ const useNumberId = options.advanced?.database?.generateId === "serial";
101
+ if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
102
+ else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`;
103
+ else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
104
+ else id = `int("id").autoincrement().primaryKey()`;
105
+ else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
106
+ else if (databaseType === "pg") id = `text('id').primaryKey()`;
107
+ else id = `text('id').primaryKey()`;
108
+ const indexes = [];
109
+ const assignIndexes = (indexes) => {
110
+ if (!indexes.length) return "";
111
+ const code = [`, (table) => [`];
112
+ for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
113
+ code.push(`]`);
114
+ return code.join("\n");
115
+ };
116
+ const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, camelCase)}", {
117
+ id: ${id},
118
+ ${Object.keys(fields).map((field) => {
119
+ const attr = fields[field];
120
+ const fieldName = attr.fieldName || field;
121
+ let type = getType(fieldName, attr);
122
+ if (attr.index && !attr.unique) indexes.push({
123
+ type: "index",
124
+ name: `${modelName}_${fieldName}_idx`,
125
+ on: fieldName
126
+ });
127
+ else if (attr.index && attr.unique) indexes.push({
128
+ type: "uniqueIndex",
129
+ name: `${modelName}_${fieldName}_uidx`,
130
+ on: fieldName
131
+ });
132
+ if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
133
+ if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
134
+ else type += `.defaultNow()`;
135
+ } else if (typeof attr.defaultValue === "string") type += `.default(${JSON.stringify(attr.defaultValue)})`;
136
+ else if (Array.isArray(attr.defaultValue)) {
137
+ const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", ");
138
+ type += `.default([${elements}])`;
139
+ } else if (typeof attr.defaultValue === "object" && attr.defaultValue !== null) type += `.default(${JSON.stringify(attr.defaultValue)})`;
140
+ else type += `.default(${attr.defaultValue})`;
141
+ if (attr.onUpdate && attr.type === "date") {
142
+ if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
143
+ }
144
+ return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
145
+ model: attr.references.model,
146
+ field: attr.references.field
147
+ })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
148
+ }).join(",\n ")}
149
+ }${assignIndexes(indexes)});`;
150
+ code += `\n${schema}\n`;
151
+ }
152
+ const schemaObjectKeys = [];
153
+ for (const tableKey in tables) {
154
+ const modelName = getModelName(tableKey);
155
+ schemaObjectKeys.push(modelName);
156
+ }
157
+ const schemaObject = `{ ${schemaObjectKeys.join(", ")} }`;
158
+ const relationsMap = /* @__PURE__ */ new Map();
159
+ for (const tableKey in tables) {
160
+ const table = tables[tableKey];
161
+ const modelName = getModelName(tableKey);
162
+ const modelRelations = [];
163
+ const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
164
+ for (const [fieldName, field] of foreignFields) {
165
+ const referencedModel = field.references.model;
166
+ const targetModelName = getModelName(referencedModel);
167
+ const fromField = getFieldName({
168
+ model: tableKey,
169
+ field: fieldName
170
+ });
171
+ const toField = getFieldName({
172
+ model: referencedModel,
173
+ field: field.references.field || "id"
174
+ });
175
+ const existingToSameModel = modelRelations.filter((r) => r.targetModel === targetModelName && r.type === "one");
176
+ const singularName = getSingularModelName(referencedModel);
177
+ const relationKey = existingToSameModel.length > 0 ? `${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}${singularName.charAt(0).toUpperCase() + singularName.slice(1)}` : singularName;
178
+ modelRelations.push({
179
+ key: relationKey,
180
+ type: "one",
181
+ targetModel: targetModelName,
182
+ fromField,
183
+ toField,
184
+ sourceModel: modelName
185
+ });
186
+ }
187
+ const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey);
188
+ for (const [otherTableKey, otherTable] of otherModels) {
189
+ const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === modelName);
190
+ if (foreignKeysPointingHere.length === 0) continue;
191
+ const otherModelName = getModelName(otherTableKey);
192
+ if (foreignKeysPointingHere.some(([_, field]) => !field.unique)) {
193
+ const nonUniqueFKs = foreignKeysPointingHere.filter(([_, field]) => !field.unique);
194
+ for (let i = 0; i < nonUniqueFKs.length; i++) {
195
+ const fkField = nonUniqueFKs[i];
196
+ if (!fkField) continue;
197
+ const [fkFieldName, fkFieldAttr] = fkField;
198
+ const fromField = getFieldName({
199
+ model: tableKey,
200
+ field: fkFieldAttr.references?.field || "id"
201
+ });
202
+ const toField = getFieldName({
203
+ model: otherTableKey,
204
+ field: fkFieldName
205
+ });
206
+ let relationKey = otherModelName;
207
+ if (!adapterConfig?.usePlural) relationKey = otherModelName.endsWith("s") ? otherModelName : `${otherModelName}s`;
208
+ if (nonUniqueFKs.length > 1) {
209
+ const capitalizedField = fkFieldName.charAt(0).toUpperCase() + fkFieldName.slice(1);
210
+ relationKey = `${relationKey}By${capitalizedField}`;
211
+ }
212
+ modelRelations.push({
213
+ key: relationKey,
214
+ type: "many",
215
+ targetModel: otherModelName,
216
+ fromField,
217
+ toField,
218
+ sourceModel: modelName
219
+ });
220
+ }
221
+ } else {
222
+ const fkField = foreignKeysPointingHere.find(([_, field]) => field.unique);
223
+ if (fkField) {
224
+ const [fkFieldName, fieldAttr] = fkField;
225
+ const fromField = getFieldName({
226
+ model: tableKey,
227
+ field: fieldAttr.references?.field || "id"
228
+ });
229
+ const toField = getFieldName({
230
+ model: otherTableKey,
231
+ field: fkFieldName
232
+ });
233
+ const relationKey = otherModelName;
234
+ modelRelations.push({
235
+ key: relationKey,
236
+ type: "one",
237
+ targetModel: otherModelName,
238
+ fromField,
239
+ toField,
240
+ sourceModel: modelName
241
+ });
242
+ }
243
+ }
244
+ }
245
+ if (modelRelations.length > 0) relationsMap.set(modelName, modelRelations);
246
+ }
247
+ let relationsString = "";
248
+ if (relationsMap.size > 0) {
249
+ const relationsEntries = [];
250
+ for (const [modelName, relations] of relationsMap.entries()) {
251
+ const relationDefs = [];
252
+ for (const relation of relations) if (relation.type === "one") relationDefs.push(` ${relation.key}: r.one.${relation.targetModel}({\n from: r.${relation.sourceModel}.${relation.fromField},\n to: r.${relation.targetModel}.${relation.toField},\n })`);
253
+ else relationDefs.push(` ${relation.key}: r.many.${relation.targetModel}({\n from: r.${relation.sourceModel}.${relation.fromField},\n to: r.${relation.targetModel}.${relation.toField},\n })`);
254
+ if (relationDefs.length > 0) relationsEntries.push(` ${modelName}: {\n${relationDefs.join(",\n")}\n }`);
255
+ }
256
+ if (relationsEntries.length > 0) relationsString = `\n\nexport const authRelations = defineRelationsPart(${schemaObject}, (r) => ({\n${relationsEntries.join(",\n")}\n}));\n`;
257
+ }
258
+ code += relationsString;
259
+ let formattedCode = code;
260
+ try {
261
+ const { format } = await new Function("moduleName", "return import(moduleName);")("prettier");
262
+ formattedCode = await format(code, { parser: "typescript" });
263
+ } catch {}
264
+ return {
265
+ code: formattedCode,
266
+ fileName: path.basename(filePath),
267
+ overwrite: fileExist,
268
+ path: filePath,
269
+ append: false
270
+ };
271
+ };
272
+ function generateImport({ databaseType, tables, options }) {
273
+ const rootImports = ["defineRelationsPart"];
274
+ const coreImports = [];
275
+ let hasBigint = false;
276
+ let hasJson = false;
277
+ for (const table of Object.values(tables)) {
278
+ for (const field of Object.values(table.fields)) {
279
+ if (field.bigint) hasBigint = true;
280
+ if (field.type === "json" || databaseType === "mysql" && (field.type === "number[]" || field.type === "string[]")) hasJson = true;
281
+ }
282
+ if (hasJson && hasBigint) break;
283
+ }
284
+ const useNumberId = options.advanced?.database?.generateId === "serial";
285
+ const useUUIDs = options.advanced?.database?.generateId === "uuid";
286
+ coreImports.push(`${databaseType}Table`);
287
+ coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
288
+ coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
289
+ coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
290
+ if (databaseType === "mysql") {
291
+ const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
292
+ if (!!useNumberId || hasNonBigintNumber) coreImports.push("int");
293
+ if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
294
+ } else if (databaseType === "pg") {
295
+ if (useUUIDs) rootImports.push("sql");
296
+ if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint)) || options.advanced?.database?.generateId === "serial") coreImports.push("integer");
297
+ } else coreImports.push("integer");
298
+ if (databaseType === "pg" && useUUIDs) coreImports.push("uuid");
299
+ if (hasJson) {
300
+ if (databaseType === "pg") coreImports.push("jsonb");
301
+ if (databaseType === "mysql") coreImports.push("json");
302
+ }
303
+ if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql");
304
+ const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
305
+ const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
306
+ if (hasIndexes) coreImports.push("index");
307
+ if (hasUniqueIndexes) coreImports.push("uniqueIndex");
308
+ return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
309
+ }
310
+ //#endregion
311
+ export { generateDrizzleSchema };
package/dist/index.mjs CHANGED
@@ -1,46 +1,47 @@
1
+ import { a as insensitiveNe, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, r as insensitiveIlike } from "./query-builders-D2eE7gbx.mjs";
1
2
  import { createAdapterFactory } from "@better-auth/core/db/adapter";
2
3
  import { logger } from "@better-auth/core/env";
3
4
  import { BetterAuthError } from "@better-auth/core/error";
4
- import { and, asc, count, desc, eq, gt, gte, ilike, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
5
- //#region src/query-builders.ts
6
- /**
7
- * Case-insensitive LIKE/ILIKE for pattern matching.
8
- * Uses ILIKE on PostgreSQL, LOWER()+LIKE on MySQL/SQLite.
9
- */
10
- function insensitiveIlike(column, pattern, provider) {
11
- return provider === "pg" ? ilike(column, pattern) : sql`LOWER(${column}) LIKE LOWER(${pattern})`;
12
- }
13
- /**
14
- * Case-insensitive IN for string arrays.
15
- */
16
- function insensitiveInArray(column, values) {
17
- if (values.length === 0) return sql`false`;
18
- return sql`LOWER(${column}) IN (${sql.join(values.map((v) => sql`LOWER(${v})`), sql`, `)})`;
19
- }
5
+ import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, isNull, like, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
6
+ //#region src/drizzle-adapter.ts
20
7
  /**
21
- * Case-insensitive NOT IN for string arrays.
8
+ * Derive the number of affected rows from a Drizzle write result.
9
+ *
10
+ * Drizzle's drivers report affected rows under different shapes: postgres-js
11
+ * exposes `rowCount`, mysql2 reports `affectedRows`/`rowsAffected` (sometimes as
12
+ * the first element of a result-header array), and better-sqlite3 uses
13
+ * `changes`. This normalizes those so write methods that depend on affected
14
+ * rows honor the adapter contract instead of leaking the raw driver result.
22
15
  */
23
- function insensitiveNotInArray(column, values) {
24
- if (values.length === 0) return sql`true`;
25
- return sql`LOWER(${column}) NOT IN (${sql.join(values.map((v) => sql`LOWER(${v})`), sql`, `)})`;
16
+ function getAffectedRowCount(result, operation, context) {
17
+ let count = 0;
18
+ if (result && typeof result === "object" && "rowCount" in result) count = result.rowCount;
19
+ else if (Array.isArray(result)) count = result.length > 0 && hasDriverRowCount(result[0]) ? readDriverRowCount(result[0]) : result.length;
20
+ else if (hasDriverRowCount(result)) count = readDriverRowCount(result);
21
+ if (typeof count !== "number" || !Number.isFinite(count)) {
22
+ logger.error(`[Drizzle Adapter] The result of the ${operation} operation is not a finite number. This is likely a bug in the adapter. Please report this issue to the Better Auth team.`, {
23
+ result,
24
+ ...context
25
+ });
26
+ throw new BetterAuthError(`Drizzle adapter ${operation} returned an invalid affected row count`);
27
+ }
28
+ return count;
26
29
  }
27
- /**
28
- * Case-insensitive equality for strings.
29
- */
30
- function insensitiveEq(column, value) {
31
- return sql`LOWER(${column}) = LOWER(${value})`;
30
+ function hasDriverRowCount(result) {
31
+ return !!result && typeof result === "object" && ("affectedRows" in result || "rowsAffected" in result || "changes" in result);
32
32
  }
33
- /**
34
- * Case-insensitive inequality for strings.
35
- */
36
- function insensitiveNe(column, value) {
37
- return sql`LOWER(${column}) <> LOWER(${value})`;
33
+ function readDriverRowCount(result) {
34
+ const r = result;
35
+ return r.affectedRows ?? r.rowsAffected ?? r.changes;
38
36
  }
39
- //#endregion
40
- //#region src/drizzle-adapter.ts
41
37
  const drizzleAdapter = (db, config) => {
42
38
  let lazyOptions = null;
43
- const createCustomAdapter = (db) => ({ getFieldName, getDefaultFieldName, options }) => {
39
+ let mysqlNoIdWarned = false;
40
+ const createCustomAdapter = (db, inTransaction = false) => ({ getFieldName, getDefaultFieldName, getDefaultModelName, options, schema: baSchema }) => {
41
+ if (config.provider === "mysql" && options.advanced?.database?.generateId === false && !mysqlNoIdWarned) {
42
+ mysqlNoIdWarned = true;
43
+ 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.");
44
+ }
44
45
  function getSchema(model) {
45
46
  const schema = config.schema || db._.fullSchema;
46
47
  if (!schema) throw new BetterAuthError("Drizzle adapter failed to initialize. Schema not found. Please provide a schema object in the adapter options object.");
@@ -62,15 +63,44 @@ const drizzleAdapter = (db, config) => {
62
63
  return w;
63
64
  }), model);
64
65
  return (await db.select().from(schemaModel).where(...clause))[0];
65
- } else if (builderVal && builderVal[0]?.id?.value) {
66
- let tId = builderVal[0]?.id?.value;
67
- if (!tId) tId = (await db.select({ id: sql`LAST_INSERT_ID()` }).from(schemaModel).orderBy(desc(schemaModel.id)).limit(1))[0].id;
68
- return (await db.select().from(schemaModel).where(eq(schemaModel.id, tId)).limit(1).execute())[0];
69
- } else if (data.id) return (await db.select().from(schemaModel).where(eq(schemaModel.id, data.id)).limit(1).execute())[0];
70
- else {
71
- if (!("id" in schemaModel)) throw new BetterAuthError(`The model "${model}" does not have an "id" field. Please use the "id" field as your primary key.`);
72
- return (await db.select().from(schemaModel).orderBy(desc(schemaModel.id)).limit(1).execute())[0];
73
66
  }
67
+ const fetchInserted = async (tx) => {
68
+ const builderId = builderVal?.[0]?.id?.value;
69
+ if (builderId) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, builderId)).limit(1).execute())[0] ?? null;
70
+ if (data.id) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, data.id)).limit(1).execute())[0] ?? null;
71
+ if (options.advanced?.database?.generateId === "serial" && schemaModel.id) {
72
+ const lastId = (await tx.select({ id: sql`LAST_INSERT_ID()` }).from(schemaModel).limit(1).execute())[0]?.id;
73
+ if (lastId) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, lastId)).limit(1).execute())[0] ?? null;
74
+ }
75
+ const modelSchema = baSchema[getDefaultModelName(model)]?.fields;
76
+ if (modelSchema) for (const [fieldKey, fieldAttr] of Object.entries(modelSchema)) {
77
+ if (!fieldAttr.unique) continue;
78
+ const dbFieldName = getFieldName({
79
+ model,
80
+ field: fieldKey
81
+ });
82
+ const val = data[dbFieldName];
83
+ if (val === void 0 || val === null) continue;
84
+ if (!schemaModel[dbFieldName]) continue;
85
+ const res = await tx.select().from(schemaModel).where(eq(schemaModel[dbFieldName], val)).limit(1).execute();
86
+ if (res[0]) return res[0];
87
+ }
88
+ const conditions = [];
89
+ for (const [key, val] of Object.entries(data)) {
90
+ if (val === void 0 || !schemaModel[key]) continue;
91
+ conditions.push(val === null ? isNull(schemaModel[key]) : eq(schemaModel[key], val));
92
+ }
93
+ if (conditions.length > 0) {
94
+ const combined = and(...conditions);
95
+ if (combined) {
96
+ const res = await tx.select().from(schemaModel).where(combined).limit(2).execute();
97
+ if (res.length === 1) return res[0];
98
+ }
99
+ }
100
+ logger.warn(`[Drizzle Adapter] Unable to safely identify the inserted "${model}" row on MySQL. Enable Better Auth ID generation or use generateId: "serial" for reliable behavior.`);
101
+ return null;
102
+ };
103
+ return inTransaction ? fetchInserted(db) : db.transaction(fetchInserted);
74
104
  };
75
105
  function convertWhereClause(where, model) {
76
106
  const schemaModel = getSchema(model);
@@ -204,10 +234,10 @@ const drizzleAdapter = (db, config) => {
204
234
  if (isInsensitive && typeof w.value === "string") return insensitiveEq(schemaModel[field], w.value);
205
235
  return eq(schemaModel[field], w.value);
206
236
  }));
207
- const clause = [];
208
- if (andGroup.length) clause.push(andClause);
209
- if (orGroup.length) clause.push(orClause);
210
- return clause;
237
+ if (andGroup.length && orGroup.length) return [and(andClause, orClause)];
238
+ if (andGroup.length) return [andClause];
239
+ if (orGroup.length) return [orClause];
240
+ return [];
211
241
  }
212
242
  function checkMissingFields(schema, model, values) {
213
243
  if (!schema) throw new BetterAuthError("Drizzle adapter failed to initialize. Drizzle Schema not found. Please provide a schema object in the adapter options object.");
@@ -397,7 +427,10 @@ const drizzleAdapter = (db, config) => {
397
427
  async updateMany({ model, where, update: values }) {
398
428
  const schemaModel = getSchema(model);
399
429
  const clause = convertWhereClause(where, model);
400
- return await db.update(schemaModel).set(values).where(...clause);
430
+ return getAffectedRowCount(await db.update(schemaModel).set(values).where(...clause), "updateMany", {
431
+ model,
432
+ where
433
+ });
401
434
  },
402
435
  async delete({ model, where }) {
403
436
  const schemaModel = getSchema(model);
@@ -407,17 +440,76 @@ const drizzleAdapter = (db, config) => {
407
440
  async deleteMany({ model, where }) {
408
441
  const schemaModel = getSchema(model);
409
442
  const clause = convertWhereClause(where, model);
410
- const res = await db.delete(schemaModel).where(...clause);
411
- let count = 0;
412
- if (res && "rowCount" in res) count = res.rowCount;
413
- else if (Array.isArray(res)) count = res.length;
414
- else if (res && ("affectedRows" in res || "rowsAffected" in res || "changes" in res)) count = res.affectedRows ?? res.rowsAffected ?? res.changes;
415
- if (typeof count !== "number") logger.error("[Drizzle Adapter] The result of the deleteMany operation is not a number. This is likely a bug in the adapter. Please report this issue to the Better Auth team.", {
416
- res,
443
+ return getAffectedRowCount(await db.delete(schemaModel).where(...clause), "deleteMany", {
417
444
  model,
418
445
  where
419
446
  });
420
- return count;
447
+ },
448
+ async consumeOne({ model, where }) {
449
+ const schemaModel = getSchema(model);
450
+ const clause = convertWhereClause(where, model);
451
+ const idField = getFieldName({
452
+ model,
453
+ field: "id"
454
+ });
455
+ const idColumn = schemaModel[idField];
456
+ if (config.provider === "mysql") {
457
+ const claimFromTransaction = async (tx) => {
458
+ const target = (await tx.select().from(schemaModel).where(...clause).for("update").limit(1))[0];
459
+ if (!target) return null;
460
+ const targetId = target[idField] ?? target.id;
461
+ if (targetId === void 0 || targetId === null || !idColumn) return null;
462
+ return getAffectedRowCount(await tx.delete(schemaModel).where(eq(idColumn, targetId)).execute(), "consumeOne", {
463
+ model,
464
+ where
465
+ }) > 0 ? target : null;
466
+ };
467
+ return inTransaction ? claimFromTransaction(db) : db.transaction(claimFromTransaction);
468
+ }
469
+ if (!idColumn) return null;
470
+ const targetIds = db.select({ id: idColumn }).from(schemaModel).where(...clause).limit(1);
471
+ return (await db.delete(schemaModel).where(inArray(idColumn, targetIds)).returning())[0] ?? null;
472
+ },
473
+ async incrementOne({ model, where, increment, set }) {
474
+ const schemaModel = getSchema(model);
475
+ const clause = convertWhereClause(where, model);
476
+ const idField = getFieldName({
477
+ model,
478
+ field: "id"
479
+ });
480
+ const idColumn = schemaModel[idField];
481
+ const assignments = {};
482
+ for (const [field, delta] of Object.entries(increment)) {
483
+ const columnName = getFieldName({
484
+ model,
485
+ field
486
+ });
487
+ const column = schemaModel[columnName];
488
+ if (!column) throw new BetterAuthError(`The field "${field}" does not exist in the schema for the model "${model}". Please update your schema.`);
489
+ assignments[columnName] = sql`${column} + ${sql.param(delta)}`;
490
+ }
491
+ if (set) for (const [field, value] of Object.entries(set)) {
492
+ const columnName = getFieldName({
493
+ model,
494
+ field
495
+ });
496
+ if (!schemaModel[columnName]) throw new BetterAuthError(`The field "${field}" does not exist in the schema for the model "${model}". Please update your schema.`);
497
+ assignments[columnName] = value;
498
+ }
499
+ if (config.provider === "mysql") {
500
+ const mutateInTransaction = async (tx) => {
501
+ const target = (await tx.select().from(schemaModel).where(...clause).for("update").limit(1))[0];
502
+ if (!target) return null;
503
+ const targetId = target[idField] ?? target.id;
504
+ if (targetId === void 0 || targetId === null || !idColumn) return null;
505
+ await tx.update(schemaModel).set(assignments).where(eq(idColumn, targetId)).execute();
506
+ return (await tx.select().from(schemaModel).where(eq(idColumn, targetId)).limit(1).execute())[0] ?? null;
507
+ };
508
+ return inTransaction ? mutateInTransaction(db) : db.transaction(mutateInTransaction);
509
+ }
510
+ if (!idColumn) return null;
511
+ const targetIds = db.select({ id: idColumn }).from(schemaModel).where(...clause).limit(1);
512
+ return (await db.update(schemaModel).set(assignments).where(inArray(idColumn, targetIds)).returning())[0] ?? null;
421
513
  },
422
514
  options: config
423
515
  };
@@ -441,8 +533,11 @@ const drizzleAdapter = (db, config) => {
441
533
  },
442
534
  transaction: config.transaction ?? false ? (cb) => db.transaction((tx) => {
443
535
  return cb(createAdapterFactory({
444
- config: adapterOptions.config,
445
- adapter: createCustomAdapter(tx)
536
+ config: {
537
+ ...adapterOptions.config,
538
+ transaction: false
539
+ },
540
+ adapter: createCustomAdapter(tx, true)
446
541
  })(lazyOptions));
447
542
  }) : false
448
543
  },
@@ -0,0 +1,52 @@
1
+ import { ilike, sql } from "drizzle-orm";
2
+ //#region src/query-builders.ts
3
+ /**
4
+ * Case-insensitive LIKE/ILIKE for pattern matching.
5
+ * Uses ILIKE on PostgreSQL, LOWER()+LIKE on MySQL/SQLite.
6
+ */
7
+ function insensitiveIlike(column, pattern, provider) {
8
+ return provider === "pg" ? ilike(column, pattern) : sql`LOWER(${column}) LIKE LOWER(${pattern})`;
9
+ }
10
+ /**
11
+ * LIKE/ILIKE with an explicit backslash escape character so callers can match a
12
+ * literal `%` or `_`. SQLite has no default LIKE escape, so the ESCAPE clause is
13
+ * always supplied. The escape character is passed as a bound parameter.
14
+ *
15
+ * This does not support MySQL's `NO_BACKSLASH_ESCAPES` sql_mode, under which the
16
+ * bound backslash is rejected as a two-character ESCAPE argument.
17
+ *
18
+ * @see https://www.sqlite.org/lang_expr.html
19
+ */
20
+ function escapedLike(column, pattern, provider, mode = "sensitive") {
21
+ const escape = "\\";
22
+ if (mode === "insensitive") return provider === "pg" ? sql`${column} ILIKE ${pattern} ESCAPE ${escape}` : sql`LOWER(${column}) LIKE LOWER(${pattern}) ESCAPE ${escape}`;
23
+ return sql`${column} LIKE ${pattern} ESCAPE ${escape}`;
24
+ }
25
+ /**
26
+ * Case-insensitive IN for string arrays.
27
+ */
28
+ function insensitiveInArray(column, values) {
29
+ if (values.length === 0) return sql`false`;
30
+ return sql`LOWER(${column}) IN (${sql.join(values.map((v) => sql`LOWER(${v})`), sql`, `)})`;
31
+ }
32
+ /**
33
+ * Case-insensitive NOT IN for string arrays.
34
+ */
35
+ function insensitiveNotInArray(column, values) {
36
+ if (values.length === 0) return sql`true`;
37
+ return sql`LOWER(${column}) NOT IN (${sql.join(values.map((v) => sql`LOWER(${v})`), sql`, `)})`;
38
+ }
39
+ /**
40
+ * Case-insensitive equality for strings.
41
+ */
42
+ function insensitiveEq(column, value) {
43
+ return sql`LOWER(${column}) = LOWER(${value})`;
44
+ }
45
+ /**
46
+ * Case-insensitive inequality for strings.
47
+ */
48
+ function insensitiveNe(column, value) {
49
+ return sql`LOWER(${column}) <> LOWER(${value})`;
50
+ }
51
+ //#endregion
52
+ export { insensitiveNe as a, insensitiveInArray as i, insensitiveEq as n, insensitiveNotInArray as o, insensitiveIlike as r, escapedLike as t };
@@ -0,0 +1,47 @@
1
+ import { DBAdapter, DBAdapterDebugLogOption } from "@better-auth/core/db/adapter";
2
+ import { BetterAuthOptions } from "@better-auth/core";
3
+
4
+ //#region src/relations-v2/index.d.ts
5
+ interface DB {
6
+ [key: string]: any;
7
+ }
8
+ interface DrizzleAdapterConfig {
9
+ /**
10
+ * The schema object that defines the tables and fields
11
+ */
12
+ schema?: Record<string, any> | undefined;
13
+ /**
14
+ * The database provider
15
+ */
16
+ provider: "pg" | "mysql" | "sqlite";
17
+ /**
18
+ * If the table names in the schema are plural
19
+ * set this to true. For example, if the schema
20
+ * has an object with a key "users" instead of "user"
21
+ */
22
+ usePlural?: boolean | undefined;
23
+ /**
24
+ * Enable debug logs for the adapter
25
+ *
26
+ * @default false
27
+ */
28
+ debugLogs?: DBAdapterDebugLogOption | undefined;
29
+ /**
30
+ * By default snake case is used for table and field names
31
+ * when the CLI is used to generate the schema. If you want
32
+ * to use camel case, set this to true.
33
+ * @default false
34
+ */
35
+ camelCase?: boolean | undefined;
36
+ /**
37
+ * Whether to execute multiple operations in a transaction.
38
+ *
39
+ * If the database doesn't support transactions,
40
+ * set this to `false` and operations will be executed sequentially.
41
+ * @default false
42
+ */
43
+ transaction?: boolean | undefined;
44
+ }
45
+ declare const drizzleAdapter: (db: DB, config: DrizzleAdapterConfig) => (options: BetterAuthOptions) => DBAdapter<BetterAuthOptions>;
46
+ //#endregion
47
+ export { DB, DrizzleAdapterConfig, drizzleAdapter };
@@ -0,0 +1,554 @@
1
+ import { a as insensitiveNe, i as insensitiveInArray, n as insensitiveEq, o as insensitiveNotInArray, t as escapedLike } from "../query-builders-D2eE7gbx.mjs";
2
+ import { createAdapterFactory } from "@better-auth/core/db/adapter";
3
+ import { logger } from "@better-auth/core/env";
4
+ import { BetterAuthError } from "@better-auth/core/error";
5
+ import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notInArray, or, sql } from "drizzle-orm";
6
+ //#region src/relations-v2/index.ts
7
+ function escapeLikePattern(value) {
8
+ if (value == null) return "";
9
+ return String(value).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
10
+ }
11
+ function readDriverRowCount(result) {
12
+ if (!result || typeof result !== "object") return void 0;
13
+ if ("affectedRows" in result) return result.affectedRows;
14
+ if ("rowsAffected" in result) return result.rowsAffected;
15
+ if ("changes" in result) return result.changes;
16
+ if ("meta" in result) {
17
+ const meta = result.meta;
18
+ if (meta && typeof meta === "object" && "changes" in meta) return meta.changes;
19
+ }
20
+ }
21
+ function hasDriverRowCount(result) {
22
+ return readDriverRowCount(result) !== void 0;
23
+ }
24
+ function getAffectedRowCount(result, operation, context) {
25
+ let count = 0;
26
+ if (result && typeof result === "object" && "rowCount" in result) count = result.rowCount;
27
+ else if (result && typeof result === "object" && typeof result.count === "number") count = result.count;
28
+ else if (Array.isArray(result)) count = result.length > 0 && hasDriverRowCount(result[0]) ? readDriverRowCount(result[0]) : result.length;
29
+ else if (hasDriverRowCount(result)) count = readDriverRowCount(result);
30
+ if (typeof count !== "number" || !Number.isFinite(count)) {
31
+ logger.error(`[Drizzle Adapter] The result of the ${operation} operation is not a finite number. This is likely a bug in the adapter. Please report this issue to the Better Auth team.`, {
32
+ result,
33
+ ...context
34
+ });
35
+ throw new BetterAuthError(`Drizzle adapter ${operation} returned an invalid affected row count`);
36
+ }
37
+ return count;
38
+ }
39
+ /**
40
+ * Maps a single Where entry to a drizzle SQL expression.
41
+ * Shared by convertWhereClause across single / AND / OR branches.
42
+ */
43
+ function applyWhereOperator(column, w, fieldLabel, provider) {
44
+ const isInsensitive = (w.mode ?? "sensitive") === "insensitive" && (typeof w.value === "string" || Array.isArray(w.value) && w.value.every((v) => typeof v === "string"));
45
+ if (w.operator === "in") {
46
+ if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${fieldLabel}" must be an array when using the "in" operator.`);
47
+ if (isInsensitive) return insensitiveInArray(column, w.value);
48
+ return inArray(column, w.value);
49
+ }
50
+ if (w.operator === "not_in") {
51
+ if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${fieldLabel}" must be an array when using the "not_in" operator.`);
52
+ if (isInsensitive) return insensitiveNotInArray(column, w.value);
53
+ return notInArray(column, w.value);
54
+ }
55
+ const likeMode = isInsensitive && typeof w.value === "string" ? "insensitive" : "sensitive";
56
+ if (w.operator === "contains") return escapedLike(column, `%${escapeLikePattern(w.value)}%`, provider, likeMode);
57
+ if (w.operator === "starts_with") return escapedLike(column, `${escapeLikePattern(w.value)}%`, provider, likeMode);
58
+ if (w.operator === "ends_with") return escapedLike(column, `%${escapeLikePattern(w.value)}`, provider, likeMode);
59
+ if (w.operator === "lt") return lt(column, w.value);
60
+ if (w.operator === "lte") return lte(column, w.value);
61
+ if (w.operator === "ne") {
62
+ if (w.value === null) return isNotNull(column);
63
+ if (isInsensitive && typeof w.value === "string") return insensitiveNe(column, w.value);
64
+ return ne(column, w.value);
65
+ }
66
+ if (w.operator === "gt") return gt(column, w.value);
67
+ if (w.operator === "gte") return gte(column, w.value);
68
+ if (w.value === null) return isNull(column);
69
+ if (isInsensitive && typeof w.value === "string") return insensitiveEq(column, w.value);
70
+ return eq(column, w.value);
71
+ }
72
+ const drizzleAdapter = (db, config) => {
73
+ let lazyOptions = null;
74
+ let mysqlNoIdWarned = false;
75
+ const createCustomAdapter = (db, inTransaction = false) => ({ getFieldName, getDefaultModelName, options, schema: baSchema }) => {
76
+ if (config.provider === "mysql" && options.advanced?.database?.generateId === false && !mysqlNoIdWarned) {
77
+ mysqlNoIdWarned = true;
78
+ 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
+ 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];
84
+ 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
+ return schemaModel;
86
+ }
87
+ /**
88
+ * Resolve the `db.query` key for a model.
89
+ *
90
+ * `db.query` is keyed by the Drizzle schema export names, which are
91
+ * often plural ("users") even when Better Auth uses singular model
92
+ * names. Try the model directly, then the `usePlural` variant, then
93
+ * scan the schema for the key pointing at the same table.
94
+ */
95
+ function getQueryModel(model) {
96
+ if (!db.query) return null;
97
+ if (db.query[model]) return model;
98
+ if (config.usePlural) {
99
+ const plural = `${model}s`;
100
+ if (db.query[plural]) return plural;
101
+ }
102
+ if (config.schema) {
103
+ const targetTable = config.schema[model];
104
+ if (targetTable) {
105
+ const relations = db._?.relations;
106
+ const fullSchema = db._?.fullSchema;
107
+ for (const key of Object.keys(db.query)) if ((relations?.[key]?.table ?? fullSchema?.[key]) === targetTable) return key;
108
+ }
109
+ }
110
+ return null;
111
+ }
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`;
120
+ }
121
+ const withReturning = async (model, builder, data, where) => {
122
+ if (config.provider !== "mysql") return (await builder.returning())[0];
123
+ const isSerialInsert = where === void 0 && options.advanced?.database?.generateId === "serial";
124
+ const insertResult = isSerialInsert ? await builder.$returningId().execute() : await builder.execute();
125
+ const schemaModel = getSchema(model);
126
+ const builderVal = builder.config?.values;
127
+ if (where?.length) {
128
+ const clause = convertWhereClause(where.map((w) => {
129
+ if (data[w.field] !== void 0) return {
130
+ ...w,
131
+ value: data[w.field]
132
+ };
133
+ return w;
134
+ }), model);
135
+ return (await db.select().from(schemaModel).where(...clause))[0];
136
+ }
137
+ const fetchInserted = async (tx) => {
138
+ const builderId = builderVal?.[0]?.id?.value;
139
+ if (builderId) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, builderId)).limit(1).execute())[0] ?? null;
140
+ if (data.id) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, data.id)).limit(1).execute())[0] ?? null;
141
+ const insertId = isSerialInsert ? insertResult?.[0]?.id : void 0;
142
+ if (insertId && schemaModel.id) return (await tx.select().from(schemaModel).where(eq(schemaModel.id, insertId)).limit(1).execute())[0] ?? null;
143
+ const modelSchema = baSchema[getDefaultModelName(model)]?.fields;
144
+ if (modelSchema) for (const [fieldKey, fieldAttr] of Object.entries(modelSchema)) {
145
+ if (!fieldAttr.unique) continue;
146
+ const dbFieldName = getFieldName({
147
+ model,
148
+ field: fieldKey
149
+ });
150
+ const val = data[dbFieldName];
151
+ if (val === void 0 || val === null) continue;
152
+ if (!schemaModel[dbFieldName]) continue;
153
+ const res = await tx.select().from(schemaModel).where(eq(schemaModel[dbFieldName], val)).limit(1).execute();
154
+ if (res[0]) return res[0];
155
+ }
156
+ const conditions = [];
157
+ for (const [key, val] of Object.entries(data)) {
158
+ if (val === void 0 || !schemaModel[key]) continue;
159
+ conditions.push(val === null ? isNull(schemaModel[key]) : eq(schemaModel[key], val));
160
+ }
161
+ if (conditions.length > 0) {
162
+ const combined = and(...conditions);
163
+ if (combined) {
164
+ const res = await tx.select().from(schemaModel).where(combined).limit(2).execute();
165
+ if (res.length === 1) return res[0];
166
+ }
167
+ }
168
+ logger.warn(`[Drizzle Adapter] Unable to safely identify the inserted "${model}" row on MySQL. Enable Better Auth ID generation or use generateId: "serial" for reliable behavior.`);
169
+ return null;
170
+ };
171
+ return inTransaction ? fetchInserted(db) : db.transaction(fetchInserted);
172
+ };
173
+ function resolveColumn(model, w) {
174
+ const schemaModel = getSchema(model);
175
+ const field = getFieldName({
176
+ model,
177
+ field: w.field
178
+ });
179
+ if (!schemaModel[field]) throw new BetterAuthError(`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`);
180
+ return {
181
+ column: schemaModel[field],
182
+ field
183
+ };
184
+ }
185
+ function convertWhereClause(where, model) {
186
+ if (!where) return [];
187
+ if (where.length === 1) {
188
+ const w = where[0];
189
+ if (!w) return [];
190
+ const { column } = resolveColumn(model, w);
191
+ return [applyWhereOperator(column, w, w.field, config.provider)];
192
+ }
193
+ const andGroup = where.filter((w) => w.connector === "AND" || !w.connector);
194
+ const orGroup = where.filter((w) => w.connector === "OR");
195
+ const combined = and(and(...andGroup.map((w) => {
196
+ const { column } = resolveColumn(model, w);
197
+ return applyWhereOperator(column, w, w.field, config.provider);
198
+ })), or(...orGroup.map((w) => {
199
+ const { column } = resolveColumn(model, w);
200
+ return applyWhereOperator(column, w, w.field, config.provider);
201
+ })));
202
+ return combined ? [combined] : [];
203
+ }
204
+ function convertNewWhereClause(where, model) {
205
+ const schemaModel = getSchema(model);
206
+ if (!where || where.length === 0) return {};
207
+ const rawCondition = (w, field) => ({ RAW: (table) => applyWhereOperator(table[field], w, w.field, config.provider) });
208
+ const convertWhereToColumn = (w) => {
209
+ const field = getFieldName({
210
+ model,
211
+ field: w.field
212
+ });
213
+ if (!schemaModel[field]) throw new BetterAuthError(`The field "${w.field}" does not exist in the schema for the model "${model}". Please update your schema.`);
214
+ const columnObj = {};
215
+ let raw;
216
+ const isInsensitive = w.mode === "insensitive" && (typeof w.value === "string" || Array.isArray(w.value) && w.value.every((v) => typeof v === "string"));
217
+ if (w.operator === "contains" || w.operator === "starts_with" || w.operator === "ends_with" || isInsensitive) raw = rawCondition(w, field);
218
+ else if (w.operator === "in") {
219
+ if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "in" operator.`);
220
+ columnObj.in = w.value;
221
+ } else if (w.operator === "not_in") {
222
+ if (!Array.isArray(w.value)) throw new BetterAuthError(`The value for the field "${w.field}" must be an array when using the "not_in" operator.`);
223
+ columnObj.notIn = w.value;
224
+ } else if (w.operator === "lt") columnObj.lt = w.value;
225
+ else if (w.operator === "lte") columnObj.lte = w.value;
226
+ else if (w.operator === "ne") if (w.value === null) columnObj.isNotNull = true;
227
+ else columnObj.ne = w.value;
228
+ else if (w.operator === "gt") columnObj.gt = w.value;
229
+ else if (w.operator === "gte") columnObj.gte = w.value;
230
+ else if (w.value === null) columnObj.isNull = true;
231
+ else columnObj.eq = w.value;
232
+ return {
233
+ field,
234
+ columnObj,
235
+ raw
236
+ };
237
+ };
238
+ if (where.length === 1) {
239
+ const w = where[0];
240
+ if (!w) return {};
241
+ const { field, columnObj, raw } = convertWhereToColumn(w);
242
+ return raw ?? { [field]: columnObj };
243
+ }
244
+ const andGroup = where.filter((w) => w.connector === "AND" || !w.connector);
245
+ const orGroup = where.filter((w) => w.connector === "OR");
246
+ const result = {};
247
+ if (andGroup.length > 0) {
248
+ const fieldMap = {};
249
+ const rawConditions = [];
250
+ for (const w of andGroup) {
251
+ const { field, columnObj, raw } = convertWhereToColumn(w);
252
+ if (raw) {
253
+ rawConditions.push(raw);
254
+ continue;
255
+ }
256
+ if (!fieldMap[field]) fieldMap[field] = [];
257
+ fieldMap[field].push(columnObj);
258
+ }
259
+ for (const [field, conditions] of Object.entries(fieldMap)) if (conditions.length === 1) result[field] = conditions[0];
260
+ else result[field] = { AND: conditions };
261
+ if (rawConditions.length > 0) result.AND = rawConditions;
262
+ }
263
+ if (orGroup.length > 0) {
264
+ const orConditions = [];
265
+ for (const w of orGroup) {
266
+ const { field, columnObj, raw } = convertWhereToColumn(w);
267
+ orConditions.push(raw ?? { [field]: columnObj });
268
+ }
269
+ if (orConditions.length > 0) result.OR = orConditions;
270
+ }
271
+ return result;
272
+ }
273
+ function checkMissingFields(schema, model, values) {
274
+ if (!schema) throw new BetterAuthError("Drizzle adapter failed to initialize. Drizzle Schema not found. Please provide a schema object in the adapter options object.");
275
+ for (const key in values) if (!schema[key]) throw new BetterAuthError(`The field "${key}" does not exist in the "${model}" Drizzle schema. Please update your drizzle schema or re-generate using "npx @better-auth/cli@latest generate".`);
276
+ }
277
+ return {
278
+ async create({ model, data: values }) {
279
+ const schemaModel = getSchema(model);
280
+ checkMissingFields(schemaModel, model, values);
281
+ return await withReturning(model, db.insert(schemaModel).values(values), values);
282
+ },
283
+ async findOne({ model, where, select, join }) {
284
+ const schemaModel = getSchema(model);
285
+ const clause = convertWhereClause(where, model);
286
+ if (options.experimental?.joins) {
287
+ const queryModel = getQueryModel(model);
288
+ if (!db.query || !queryModel) {
289
+ logger.error(`[# Drizzle Adapter]: The model "${model}" was not found in the query object. Please update your Drizzle schema to include relations or re-generate using "npx @better-auth/cli@latest generate".`);
290
+ logger.info("Falling back to regular query");
291
+ } else {
292
+ let includes;
293
+ const pluralJoinResults = [];
294
+ if (join) {
295
+ includes = {};
296
+ const joinEntries = Object.entries(join);
297
+ for (const [model, joinAttr] of joinEntries) {
298
+ const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
299
+ const isUnique = joinAttr.relation === "one-to-one";
300
+ const relationKey = getJoinRelationKey(model, isUnique);
301
+ includes[relationKey] = isUnique ? true : { limit };
302
+ if (!isUnique) pluralJoinResults.push({
303
+ key: relationKey,
304
+ target: model
305
+ });
306
+ }
307
+ }
308
+ const clause = convertNewWhereClause(where, model);
309
+ const res = await db.query[queryModel].findFirst({
310
+ where: clause,
311
+ columns: select?.length && select.length > 0 ? select.reduce((acc, field) => {
312
+ acc[getFieldName({
313
+ model,
314
+ field
315
+ })] = true;
316
+ return acc;
317
+ }, {}) : void 0,
318
+ with: includes
319
+ });
320
+ if (res) for (const { key, target } of pluralJoinResults) {
321
+ if (key === target) continue;
322
+ res[target] = res[key];
323
+ delete res[key];
324
+ }
325
+ return res;
326
+ }
327
+ }
328
+ const res = await db.select(select?.length && select.length > 0 ? select.reduce((acc, field) => {
329
+ const fieldName = getFieldName({
330
+ model,
331
+ field
332
+ });
333
+ return {
334
+ ...acc,
335
+ [fieldName]: schemaModel[fieldName]
336
+ };
337
+ }, {}) : void 0).from(schemaModel).where(...clause);
338
+ if (!res.length) return null;
339
+ return res[0];
340
+ },
341
+ async findMany({ model, where, sortBy, limit, select, offset, join }) {
342
+ const schemaModel = getSchema(model);
343
+ const clause = where ? convertWhereClause(where, model) : [];
344
+ const sortFn = sortBy?.direction === "desc" ? desc : asc;
345
+ if (options.experimental?.joins) {
346
+ const queryModel = getQueryModel(model);
347
+ if (!db.query || !queryModel) {
348
+ logger.error(`[# Drizzle Adapter]: The model "${model}" was not found in the query object. Please update your Drizzle schema to include relations or re-generate using "npx @better-auth/cli@latest generate".`);
349
+ logger.info("Falling back to regular query");
350
+ } else {
351
+ let includes;
352
+ const pluralJoinResults = [];
353
+ if (join) {
354
+ includes = {};
355
+ const joinEntries = Object.entries(join);
356
+ for (const [model, joinAttr] of joinEntries) {
357
+ const isUnique = joinAttr.relation === "one-to-one";
358
+ const limit = joinAttr.limit ?? options.advanced?.database?.defaultFindManyLimit ?? 100;
359
+ const relationKey = getJoinRelationKey(model, isUnique);
360
+ includes[relationKey] = isUnique ? true : { limit };
361
+ if (!isUnique) pluralJoinResults.push({
362
+ key: relationKey,
363
+ target: model
364
+ });
365
+ }
366
+ }
367
+ let orderBy = void 0;
368
+ if (sortBy?.field) orderBy = { [getFieldName({
369
+ model,
370
+ field: sortBy.field
371
+ })]: sortBy.direction === "desc" ? "desc" : "asc" };
372
+ const res = await db.query[queryModel].findMany({
373
+ where: where ? convertNewWhereClause(where, model) : void 0,
374
+ with: includes,
375
+ columns: select?.length && select.length > 0 ? select.reduce((acc, field) => {
376
+ acc[getFieldName({
377
+ model,
378
+ field
379
+ })] = true;
380
+ return acc;
381
+ }, {}) : void 0,
382
+ limit: limit ?? 100,
383
+ offset: offset ?? 0,
384
+ orderBy
385
+ });
386
+ if (res) for (const item of res) for (const { key, target } of pluralJoinResults) {
387
+ if (key === target) continue;
388
+ item[target] = item[key];
389
+ delete item[key];
390
+ }
391
+ return res;
392
+ }
393
+ }
394
+ let builder = db.select(select?.length && select.length > 0 ? select.reduce((acc, field) => {
395
+ const fieldName = getFieldName({
396
+ model,
397
+ field
398
+ });
399
+ return {
400
+ ...acc,
401
+ [fieldName]: schemaModel[fieldName]
402
+ };
403
+ }, {}) : void 0).from(schemaModel);
404
+ const effectiveLimit = limit;
405
+ const effectiveOffset = offset;
406
+ if (typeof effectiveLimit !== "undefined") builder = builder.limit(effectiveLimit);
407
+ if (typeof effectiveOffset !== "undefined") builder = builder.offset(effectiveOffset);
408
+ if (sortBy?.field) builder = builder.orderBy(sortFn(schemaModel[getFieldName({
409
+ model,
410
+ field: sortBy?.field
411
+ })]));
412
+ return await builder.where(...clause);
413
+ },
414
+ async count({ model, where }) {
415
+ const schemaModel = getSchema(model);
416
+ const clause = where ? convertWhereClause(where, model) : [];
417
+ return (await db.select({ count: count() }).from(schemaModel).where(...clause))[0].count;
418
+ },
419
+ async update({ model, where, update: values }) {
420
+ const schemaModel = getSchema(model);
421
+ const clause = convertWhereClause(where, model);
422
+ return await withReturning(model, db.update(schemaModel).set(values).where(...clause), values, where);
423
+ },
424
+ async updateMany({ model, where, update: values }) {
425
+ const schemaModel = getSchema(model);
426
+ const clause = convertWhereClause(where, model);
427
+ return getAffectedRowCount(await db.update(schemaModel).set(values).where(...clause), "updateMany", {
428
+ model,
429
+ where
430
+ });
431
+ },
432
+ async delete({ model, where }) {
433
+ const schemaModel = getSchema(model);
434
+ const clause = convertWhereClause(where, model);
435
+ return await db.delete(schemaModel).where(...clause);
436
+ },
437
+ async deleteMany({ model, where }) {
438
+ const schemaModel = getSchema(model);
439
+ const clause = convertWhereClause(where, model);
440
+ return getAffectedRowCount(await db.delete(schemaModel).where(...clause), "deleteMany", {
441
+ model,
442
+ where
443
+ });
444
+ },
445
+ async consumeOne({ model, where }) {
446
+ const schemaModel = getSchema(model);
447
+ const clause = convertWhereClause(where, model);
448
+ const idField = getFieldName({
449
+ model,
450
+ field: "id"
451
+ });
452
+ const idColumn = schemaModel[idField];
453
+ if (config.provider === "mysql") {
454
+ const claimFromTransaction = async (tx) => {
455
+ const target = (await tx.select().from(schemaModel).where(...clause).for("update").limit(1))[0];
456
+ if (!target) return null;
457
+ const targetId = target[idField] ?? target.id;
458
+ if (targetId === void 0 || targetId === null || !idColumn) return null;
459
+ return getAffectedRowCount(await tx.delete(schemaModel).where(eq(idColumn, targetId)).execute(), "consumeOne", {
460
+ model,
461
+ where
462
+ }) > 0 ? target : null;
463
+ };
464
+ return inTransaction ? claimFromTransaction(db) : db.transaction(claimFromTransaction);
465
+ }
466
+ if (!idColumn) return null;
467
+ const targetIds = db.select({ id: idColumn }).from(schemaModel).where(...clause).limit(1);
468
+ return (await db.delete(schemaModel).where(inArray(idColumn, targetIds)).returning())[0] ?? null;
469
+ },
470
+ async incrementOne({ model, where, increment, set }) {
471
+ const schemaModel = getSchema(model);
472
+ const clause = convertWhereClause(where, model);
473
+ const idField = getFieldName({
474
+ model,
475
+ field: "id"
476
+ });
477
+ const idColumn = schemaModel[idField];
478
+ const assignments = {};
479
+ for (const [field, delta] of Object.entries(increment)) {
480
+ const columnName = getFieldName({
481
+ model,
482
+ field
483
+ });
484
+ const column = schemaModel[columnName];
485
+ if (!column) throw new BetterAuthError(`The field "${field}" does not exist in the schema for the model "${model}". Please update your schema.`);
486
+ assignments[columnName] = sql`${column} + ${sql.param(delta)}`;
487
+ }
488
+ if (set) for (const [field, value] of Object.entries(set)) {
489
+ const columnName = getFieldName({
490
+ model,
491
+ field
492
+ });
493
+ if (!schemaModel[columnName]) throw new BetterAuthError(`The field "${field}" does not exist in the schema for the model "${model}". Please update your schema.`);
494
+ assignments[columnName] = value;
495
+ }
496
+ if (config.provider === "mysql") {
497
+ const mutateInTransaction = async (tx) => {
498
+ const target = (await tx.select().from(schemaModel).where(...clause).for("update").limit(1))[0];
499
+ if (!target) return null;
500
+ const targetId = target[idField] ?? target.id;
501
+ if (targetId === void 0 || targetId === null || !idColumn) return null;
502
+ await tx.update(schemaModel).set(assignments).where(eq(idColumn, targetId)).execute();
503
+ return (await tx.select().from(schemaModel).where(eq(idColumn, targetId)).limit(1).execute())[0] ?? null;
504
+ };
505
+ return inTransaction ? mutateInTransaction(db) : db.transaction(mutateInTransaction);
506
+ }
507
+ if (!idColumn) return null;
508
+ const targetIds = db.select({ id: idColumn }).from(schemaModel).where(...clause).limit(1);
509
+ return (await db.update(schemaModel).set(assignments).where(inArray(idColumn, targetIds)).returning())[0] ?? null;
510
+ },
511
+ async createSchema(props) {
512
+ const { generateDrizzleSchema } = await import("../generate-drizzle-schema-BdVT4xKv.mjs");
513
+ return await generateDrizzleSchema({
514
+ adapterConfig: config,
515
+ options,
516
+ provider: config.provider,
517
+ camelCase: config.camelCase,
518
+ file: props.file,
519
+ tables: props.tables
520
+ });
521
+ },
522
+ options: config
523
+ };
524
+ };
525
+ let adapterOptions = null;
526
+ adapterOptions = {
527
+ config: {
528
+ adapterId: "drizzle",
529
+ adapterName: "Drizzle Adapter",
530
+ usePlural: config.usePlural ?? false,
531
+ debugLogs: config.debugLogs ?? false,
532
+ supportsUUIDs: config.provider === "pg" ? true : false,
533
+ supportsJSON: true,
534
+ supportsArrays: true,
535
+ transaction: config.transaction ?? false ? (cb) => db.transaction((tx) => {
536
+ return cb(createAdapterFactory({
537
+ config: {
538
+ ...adapterOptions.config,
539
+ transaction: false
540
+ },
541
+ adapter: createCustomAdapter(tx, true)
542
+ })(lazyOptions));
543
+ }) : false
544
+ },
545
+ adapter: createCustomAdapter(db)
546
+ };
547
+ const adapter = createAdapterFactory(adapterOptions);
548
+ return (options) => {
549
+ lazyOptions = options;
550
+ return adapter(options);
551
+ };
552
+ };
553
+ //#endregion
554
+ export { drizzleAdapter };
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@better-auth/drizzle-adapter",
3
- "version": "1.7.0-beta.1",
3
+ "version": "1.7.0-beta.10",
4
+ "bugs": {
5
+ "url": "https://github.com/better-auth/better-auth/issues"
6
+ },
4
7
  "description": "Drizzle adapter for Better Auth",
5
8
  "type": "module",
6
9
  "license": "MIT",
@@ -29,15 +32,20 @@
29
32
  "types": "./dist/index.d.mts",
30
33
  "exports": {
31
34
  ".": {
32
- "dev-source": "./src/index.ts",
33
35
  "types": "./dist/index.d.mts",
36
+ "dev-source": "./src/index.ts",
34
37
  "default": "./dist/index.mjs"
38
+ },
39
+ "./relations-v2": {
40
+ "types": "./dist/relations-v2/index.d.mts",
41
+ "dev-source": "./src/relations-v2/index.ts",
42
+ "default": "./dist/relations-v2/index.mjs"
35
43
  }
36
44
  },
37
45
  "peerDependencies": {
38
- "@better-auth/utils": "0.4.0",
39
- "drizzle-orm": ">=0.41.0",
40
- "@better-auth/core": "^1.7.0-beta.1"
46
+ "@better-auth/utils": "0.4.2",
47
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
48
+ "@better-auth/core": "^1.7.0-beta.10"
41
49
  },
42
50
  "peerDependenciesMeta": {
43
51
  "drizzle-orm": {
@@ -45,11 +53,11 @@
45
53
  }
46
54
  },
47
55
  "devDependencies": {
48
- "@better-auth/utils": "0.4.0",
56
+ "@better-auth/utils": "0.4.2",
49
57
  "drizzle-orm": "^0.45.2",
50
58
  "tsdown": "0.21.1",
51
59
  "typescript": "^5.9.3",
52
- "@better-auth/core": "1.7.0-beta.1"
60
+ "@better-auth/core": "1.7.0-beta.10"
53
61
  },
54
62
  "scripts": {
55
63
  "build": "tsdown",