@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.
- package/dist/generate-drizzle-schema-BdVT4xKv.mjs +311 -0
- package/dist/index.mjs +151 -56
- package/dist/query-builders-D2eE7gbx.mjs +52 -0
- package/dist/relations-v2/index.d.mts +47 -0
- package/dist/relations-v2/index.mjs +554 -0
- package/package.json +15 -7
|
@@ -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,
|
|
5
|
-
//#region src/
|
|
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
|
-
*
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
if (andGroup.length)
|
|
209
|
-
if (orGroup.length)
|
|
210
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
445
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"drizzle-orm": ">=0.
|
|
40
|
-
"@better-auth/core": "^1.7.0-beta.
|
|
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.
|
|
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.
|
|
60
|
+
"@better-auth/core": "1.7.0-beta.10"
|
|
53
61
|
},
|
|
54
62
|
"scripts": {
|
|
55
63
|
"build": "tsdown",
|