auth 1.7.0-rc.0 → 1.7.0-rc.2

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/api.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { DBAdapter, DBAdapter as DBAdapter$1 } from "@better-auth/core/db/adapter";
2
2
  import { BetterAuthOptions } from "@better-auth/core";
3
-
4
3
  //#region src/generators/types.d.ts
5
4
  interface SchemaGeneratorResult {
6
5
  code?: string;
package/dist/api.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
2
3
  import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
3
4
  import { initGetFieldName, initGetModelName } from "better-auth/adapters";
4
5
  import { getAuthTables } from "better-auth/db";
@@ -6,22 +7,36 @@ import prettier from "prettier";
6
7
  import { getMigrations } from "better-auth/db/migration";
7
8
  import fs from "node:fs/promises";
8
9
  import path from "node:path";
10
+ import { BetterAuthError } from "@better-auth/core/error";
9
11
  import { produceSchema } from "@mrleebo/prisma-ast";
10
12
  //#region src/generators/drizzle.ts
11
13
  function convertToSnakeCase(str, camelCase) {
12
14
  return camelCase ? str : toSnakeCase(str);
13
15
  }
16
+ function toValidIdentifier(str) {
17
+ let result = str.replace(/[^a-zA-Z0-9_]/g, "").replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/^[0-9]/, "_$&");
18
+ if (result.length > 0 && result[0].match(/[A-Z]/)) result = result[0].toLowerCase() + result.slice(1);
19
+ return result || "schema";
20
+ }
14
21
  const generateDrizzleSchema = async ({ options, file, adapter }) => {
15
22
  const tables = getAuthTables(options);
16
23
  const filePath = file || "./auth-schema.ts";
17
24
  const databaseType = adapter.options?.provider;
25
+ const schemaName = adapter.options?.schemaName;
18
26
  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`);
19
27
  const fileExist = existsSync(filePath);
20
28
  let code = generateImport({
21
29
  databaseType,
22
30
  tables,
23
- options
31
+ options,
32
+ schemaName
24
33
  });
34
+ let schemaVarName;
35
+ if (databaseType === "pg" && schemaName) {
36
+ schemaVarName = `${toValidIdentifier(schemaName)}Schema`;
37
+ if (schemaVarName === "pgSchema") schemaVarName = "pgCustomSchema";
38
+ code += `\nconst ${schemaVarName} = pgSchema(${JSON.stringify(schemaName)});\n\n`;
39
+ }
25
40
  const getModelName = initGetModelName({
26
41
  schema: tables,
27
42
  usePlural: adapter.options?.adapterConfig?.usePlural
@@ -39,12 +54,18 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
39
54
  const customModelName = table.modelName || tableKey;
40
55
  return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
41
56
  });
57
+ const resolvedIndexesByTable = resolveDatabaseSchemaIndexes(Object.keys(tables).filter((tableKey) => !isMigrationDisabled(tableKey)).map((tableKey) => ({
58
+ fields: tables[tableKey].fields,
59
+ indexes: tables[tableKey].indexes,
60
+ tableName: getModelName(tableKey)
61
+ })));
42
62
  for (const tableKey in tables) {
43
63
  const table = tables[tableKey];
44
64
  if (isMigrationDisabled(tableKey)) continue;
45
65
  const modelName = getModelName(tableKey);
46
66
  const fields = table.fields;
47
- function getType(name, field) {
67
+ const resolvedTableIndexes = resolvedIndexesByTable.get(modelName) ?? [];
68
+ function getType(name, field, tableIndexStringLength) {
48
69
  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`);
49
70
  name = convertToSnakeCase(name, adapter.options?.camelCase);
50
71
  if (field.references?.field === "id") {
@@ -70,7 +91,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
70
91
  string: {
71
92
  sqlite: `text('${name}')`,
72
93
  pg: `text('${name}')`,
73
- 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}')`
94
+ mysql: tableIndexStringLength ? `varchar('${name}', { length: ${tableIndexStringLength} })` : 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}')`
74
95
  },
75
96
  boolean: {
76
97
  sqlite: `integer('${name}', { mode: 'boolean' })`,
@@ -119,30 +140,35 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
119
140
  const assignIndexes = (indexes) => {
120
141
  if (!indexes.length) return "";
121
142
  const code = [`, (table) => [`];
122
- for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
143
+ for (const index of indexes) code.push(` ${index.type}(${JSON.stringify(index.name)}).on(${index.on.map((fieldName) => `table.${fieldName}`).join(", ")}),`);
123
144
  code.push(`]`);
124
145
  return code.join("\n");
125
146
  };
126
- const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
147
+ for (const tableIndex of resolvedTableIndexes) indexes.push({
148
+ type: tableIndex.unique ? "uniqueIndex" : "index",
149
+ name: tableIndex.name,
150
+ on: tableIndex.columns
151
+ });
152
+ const schema = `export const ${modelName} = ${databaseType === "pg" && schemaName && schemaVarName ? `${schemaVarName}.table` : `${databaseType}Table`}("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
127
153
  id: ${id},
128
154
  ${Object.keys(fields).map((field) => {
129
155
  const attr = fields[field];
130
156
  const fieldName = attr.fieldName || field;
131
- let type = getType(fieldName, attr);
157
+ let type = getType(fieldName, attr, databaseType === "mysql" ? getDatabaseIndexStringLength({
158
+ columnName: fieldName,
159
+ dialect: "mysql",
160
+ fields,
161
+ indexes: resolvedTableIndexes
162
+ }) : void 0);
132
163
  if (attr.index && !attr.unique) indexes.push({
133
164
  type: "index",
134
- name: `${modelName}_${fieldName}_idx`,
135
- on: fieldName
136
- });
137
- else if (attr.index && attr.unique) indexes.push({
138
- type: "uniqueIndex",
139
- name: `${modelName}_${fieldName}_uidx`,
140
- on: fieldName
165
+ name: getDatabaseFieldIndexName(modelName, fieldName, false),
166
+ on: [fieldName]
141
167
  });
142
168
  if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
143
169
  if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
144
170
  else type += `.defaultNow()`;
145
- } else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
171
+ } else if (typeof attr.defaultValue === "string") type += `.default(${JSON.stringify(attr.defaultValue)})`;
146
172
  else if (Array.isArray(attr.defaultValue)) {
147
173
  const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", ");
148
174
  type += `.default([${elements}])`;
@@ -167,12 +193,25 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
167
193
  const modelName = getModelName(tableKey);
168
194
  const oneRelations = [];
169
195
  const manyRelations = [];
170
- const manyRelationsSet = /* @__PURE__ */ new Set();
171
196
  const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
197
+ const foreignFieldCounts = /* @__PURE__ */ new Map();
198
+ for (const [_, field] of foreignFields) {
199
+ const referencedModel = getModelName(field.references.model);
200
+ foreignFieldCounts.set(referencedModel, (foreignFieldCounts.get(referencedModel) ?? 0) + 1);
201
+ }
202
+ const usedOneRelationKeys = /* @__PURE__ */ new Set();
172
203
  for (const [fieldName, field] of foreignFields) {
173
204
  const referencedModel = field.references.model;
174
205
  if (isMigrationDisabled(referencedModel)) continue;
175
- const relationKey = getSingularModelName(referencedModel);
206
+ const hasMultipleRelations = (foreignFieldCounts.get(getModelName(referencedModel)) ?? 0) > 1;
207
+ let relationKey = hasMultipleRelations ? fieldName.replace(/Id$/, "") : getSingularModelName(referencedModel);
208
+ if (usedOneRelationKeys.has(relationKey)) relationKey = fieldName;
209
+ if (usedOneRelationKeys.has(relationKey)) {
210
+ let suffix = 2;
211
+ while (usedOneRelationKeys.has(`${relationKey}_${suffix}`)) suffix++;
212
+ relationKey = `${relationKey}_${suffix}`;
213
+ }
214
+ usedOneRelationKeys.add(relationKey);
176
215
  const fieldRef = `${getModelName(tableKey)}.${getFieldName({
177
216
  model: tableKey,
178
217
  field: fieldName
@@ -185,81 +224,49 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
185
224
  key: relationKey,
186
225
  model: getModelName(referencedModel),
187
226
  type: "one",
227
+ relationName: hasMultipleRelations ? `${getModelName(tableKey)}_${fieldName}` : void 0,
188
228
  reference: {
189
229
  field: fieldRef,
190
- references: referenceRef,
191
- fieldName
230
+ references: referenceRef
192
231
  }
193
232
  });
194
233
  }
195
234
  const otherModels = Object.entries(tables).filter(([modelName, otherTable]) => modelName !== tableKey && !otherTable.disableMigrations);
196
- const modelRelationsMap = /* @__PURE__ */ new Map();
197
235
  for (const [modelName, otherTable] of otherModels) {
198
236
  const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
199
237
  if (foreignKeysPointingHere.length === 0) continue;
200
- const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
201
- const hasMany = foreignKeysPointingHere.some(([_, field]) => !field.unique);
202
- modelRelationsMap.set(modelName, {
203
- modelName,
204
- hasUnique,
205
- hasMany
206
- });
207
- }
208
- for (const { modelName, hasMany } of modelRelationsMap.values()) {
209
- const relationType = hasMany ? "many" : "one";
210
- let relationKey = getModelName(modelName);
211
- if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
212
- if (!manyRelationsSet.has(relationKey)) {
213
- manyRelationsSet.add(relationKey);
238
+ for (const [fieldName, field] of foreignKeysPointingHere) {
239
+ const relationType = field.unique ? "one" : "many";
240
+ let relationKey = getModelName(modelName);
241
+ if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
242
+ const hasMultipleRelations = foreignKeysPointingHere.length > 1;
243
+ if (hasMultipleRelations) relationKey = `${relationKey}By${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
214
244
  manyRelations.push({
215
245
  key: relationKey,
216
246
  model: getModelName(modelName),
217
- type: relationType
247
+ type: relationType,
248
+ relationName: hasMultipleRelations ? `${getModelName(modelName)}_${fieldName}` : void 0
218
249
  });
219
250
  }
220
251
  }
221
- const relationsByModel = /* @__PURE__ */ new Map();
222
- for (const relation of oneRelations) if (relation.reference) {
223
- const modelKey = relation.key;
224
- if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
225
- relationsByModel.get(modelKey).push(relation);
226
- }
227
- const duplicateRelations = [];
228
- const singleRelations = [];
229
- for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
230
- else singleRelations.push(relations[0]);
231
- for (const relation of duplicateRelations) if (relation.reference) {
232
- const fieldName = relation.reference.fieldName;
233
- const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
234
- ${relation.key}: one(${relation.model}, {
252
+ const hasForwardOne = oneRelations.length > 0;
253
+ const hasReverseOne = manyRelations.some((relation) => relation.type === "one");
254
+ const hasReverseMany = manyRelations.some((relation) => relation.type === "many");
255
+ const hasOne = hasForwardOne || hasReverseOne;
256
+ const hasMany = hasReverseMany;
257
+ const renderOneRelation = (relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
235
258
  fields: [${relation.reference.field}],
236
259
  references: [${relation.reference.references}],
237
- })
238
- }))`;
239
- relationsString += `\n${tableRelation}\n`;
240
- }
241
- const hasOne = singleRelations.length > 0;
242
- const hasMany = manyRelations.length > 0;
243
- if (hasOne && hasMany) {
244
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
245
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
246
- fields: [${relation.reference.field}],
247
- references: [${relation.reference.references}],
248
- })` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
249
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
250
- }))`;
251
- relationsString += `\n${tableRelation}\n`;
252
- } else if (hasOne) {
253
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
254
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
255
- fields: [${relation.reference.field}],
256
- references: [${relation.reference.references}],
257
- })` : "").filter((x) => x !== "").join(",\n ")}
258
- }))`;
259
- relationsString += `\n${tableRelation}\n`;
260
- } else if (hasMany) {
261
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
262
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
260
+ ${relation.relationName ? `relationName: "${relation.relationName}",` : ""}
261
+ })` : "";
262
+ const renderReverseRelation = ({ key, model, type, relationName }) => {
263
+ return ` ${key}: ${type === "one" ? "one" : "many"}(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`;
264
+ };
265
+ if (hasOne || hasMany) {
266
+ const helpers = [hasOne ? "one" : null, hasMany ? "many" : null].filter(Boolean).join(", ");
267
+ const relationEntries = [...oneRelations.map(renderOneRelation).filter((x) => x !== ""), ...manyRelations.map(renderReverseRelation)].join(",\n ");
268
+ const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ ${helpers} }) => ({
269
+ ${relationEntries}
263
270
  }))`;
264
271
  relationsString += `\n${tableRelation}\n`;
265
272
  }
@@ -271,7 +278,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
271
278
  overwrite: fileExist
272
279
  };
273
280
  };
274
- function generateImport({ databaseType, tables, options }) {
281
+ function generateImport({ databaseType, tables, options, schemaName }) {
275
282
  const rootImports = ["relations"];
276
283
  const coreImports = [];
277
284
  let hasBigint = false;
@@ -285,7 +292,8 @@ function generateImport({ databaseType, tables, options }) {
285
292
  }
286
293
  const useNumberId = options.advanced?.database?.generateId === "serial";
287
294
  const useUUIDs = options.advanced?.database?.generateId === "uuid";
288
- coreImports.push(`${databaseType}Table`);
295
+ if (databaseType === "pg" && schemaName) coreImports.push("pgSchema");
296
+ if (!(databaseType === "pg" && schemaName)) coreImports.push(`${databaseType}Table`);
289
297
  coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
290
298
  coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
291
299
  coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
@@ -305,8 +313,8 @@ function generateImport({ databaseType, tables, options }) {
305
313
  if (databaseType === "mysql") coreImports.push("json");
306
314
  }
307
315
  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");
308
- const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
309
- const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
316
+ const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique) || (table.indexes?.some((index) => !index.unique) ?? false));
317
+ const hasUniqueIndexes = Object.values(tables).some((table) => table.indexes?.some((index) => index.unique) ?? false);
310
318
  if (hasIndexes) coreImports.push("index");
311
319
  if (hasUniqueIndexes) coreImports.push("uniqueIndex");
312
320
  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`;
@@ -340,11 +348,58 @@ function getPrismaVersion(cwd) {
340
348
  }
341
349
  //#endregion
342
350
  //#region src/generators/prisma.ts
351
+ function isRecord(value) {
352
+ return typeof value === "object" && value !== null;
353
+ }
354
+ function parsePrismaStringLiteral(value) {
355
+ if (typeof value !== "string") return void 0;
356
+ try {
357
+ const parsed = JSON.parse(value);
358
+ return typeof parsed === "string" ? parsed : void 0;
359
+ } catch {
360
+ return;
361
+ }
362
+ }
363
+ function getPrismaIndexDefinition(property) {
364
+ if (!isRecord(property) || property.type !== "attribute" || property.kind !== "object" || property.name !== "index" && property.name !== "unique" || !Array.isArray(property.args)) return;
365
+ const propertyArgs = property.args;
366
+ let columns;
367
+ let mappedName;
368
+ let validFullColumns = false;
369
+ for (const argument of propertyArgs) {
370
+ if (!isRecord(argument) || !isRecord(argument.value)) continue;
371
+ const value = argument.value;
372
+ if (value.type === "array" && Array.isArray(value.args) && value.args.every((column) => typeof column === "string")) {
373
+ columns = value.args.map((column) => String(column));
374
+ validFullColumns = true;
375
+ } else if (value.type === "keyValue" && value.key === "map") mappedName = parsePrismaStringLiteral(value.value);
376
+ }
377
+ return {
378
+ columns: columns ?? [],
379
+ mappedName,
380
+ setMappedName(name) {
381
+ propertyArgs.push({
382
+ type: "attributeArgument",
383
+ value: {
384
+ type: "keyValue",
385
+ key: "map",
386
+ value: JSON.stringify(name)
387
+ }
388
+ });
389
+ },
390
+ unique: property.name === "unique",
391
+ validFullColumns
392
+ };
393
+ }
394
+ function prismaIndexMatches(existing, configured) {
395
+ return existing.validFullColumns && existing.unique === (configured.unique ?? false) && existing.columns.length === configured.columns.length && existing.columns.every((column, position) => column === configured.columns[position]);
396
+ }
343
397
  const generatePrismaSchema = async ({ adapter, options, file }) => {
344
398
  const provider = adapter.options?.provider || "postgresql";
345
399
  const tables = getAuthTables(options);
346
400
  const filePath = file || "./prisma/schema.prisma";
347
- const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
401
+ const resolvedFilePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
402
+ const schemaPrismaExist = existsSync(resolvedFilePath);
348
403
  const getModelName = initGetModelName({
349
404
  schema: getAuthTables(options),
350
405
  usePlural: adapter.options?.adapterConfig?.usePlural
@@ -358,8 +413,16 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
358
413
  const customModelName = table.modelName || tableKey;
359
414
  return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
360
415
  });
416
+ const resolvedIndexesByTable = resolveDatabaseSchemaIndexes(Object.keys(tables).filter((tableKey) => !isMigrationDisabled(tableKey)).map((tableKey) => {
417
+ const customModelName = tables[tableKey]?.modelName || tableKey;
418
+ return {
419
+ fields: tables[tableKey].fields,
420
+ indexes: tables[tableKey].indexes,
421
+ tableName: getModelName(customModelName)
422
+ };
423
+ }));
361
424
  let schemaPrisma = "";
362
- if (schemaPrismaExist) schemaPrisma = await fs.readFile(path.join(process.cwd(), filePath), "utf-8");
425
+ if (schemaPrismaExist) schemaPrisma = await fs.readFile(resolvedFilePath, "utf-8");
363
426
  else schemaPrisma = getNewPrisma(provider, process.cwd());
364
427
  const prismaVersion = getPrismaVersion(process.cwd());
365
428
  if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
@@ -410,8 +473,10 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
410
473
  const customModelName = tables[table]?.modelName || table;
411
474
  const modelName = capitalizeFirstLetter(getModelName(customModelName));
412
475
  const fields = tables[table]?.fields;
476
+ const resolvedTableIndexes = resolvedIndexesByTable.get(getModelName(customModelName)) ?? [];
413
477
  function getType({ isBigint, isOptional, type }) {
414
478
  if (type === "string") return isOptional ? "String?" : "String";
479
+ if (Array.isArray(type)) return isOptional ? "String?" : "String";
415
480
  if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
416
481
  if (type === "number") return isOptional ? "Int?" : "Int";
417
482
  if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
@@ -477,6 +542,18 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
477
542
  isAlreadyExist.array = fieldTypeParts.isArray || void 0;
478
543
  }
479
544
  }
545
+ if (provider === "mysql" && (attr.type === "string" || Array.isArray(attr.type)) && typeof isAlreadyExist.fieldType === "string" && getFieldTypeParts(isAlreadyExist.fieldType).fieldType === "String") {
546
+ const tableIndexStringLength = getDatabaseIndexStringLength({
547
+ columnName: fieldName,
548
+ dialect: "mysql",
549
+ fields: fields ?? {},
550
+ indexes: resolvedTableIndexes
551
+ });
552
+ if (tableIndexStringLength) {
553
+ isAlreadyExist.attributes = isAlreadyExist.attributes?.filter((attribute) => attribute.group !== "db");
554
+ builder.model(modelName).field(fieldName).attribute(`db.VarChar(${tableIndexStringLength})`);
555
+ }
556
+ }
480
557
  continue;
481
558
  }
482
559
  }
@@ -544,7 +621,31 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
544
621
  })}], onDelete: ${action})`;
545
622
  builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField);
546
623
  }
547
- if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
624
+ if (provider === "mysql" && (attr.type === "string" || Array.isArray(attr.type))) {
625
+ const tableIndexStringLength = getDatabaseIndexStringLength({
626
+ columnName: fieldName,
627
+ dialect: "mysql",
628
+ fields: fields ?? {},
629
+ indexes: resolvedTableIndexes
630
+ });
631
+ const nativeType = tableIndexStringLength ? `db.VarChar(${tableIndexStringLength})` : !attr.unique && !attr.references ? "db.Text" : void 0;
632
+ if (nativeType) builder.model(modelName).field(fieldName).attribute(nativeType);
633
+ }
634
+ }
635
+ for (const tableIndex of resolvedTableIndexes) {
636
+ const attributeName = tableIndex.unique ? "unique" : "index";
637
+ const existingIndexes = prismaModel?.properties.map(getPrismaIndexDefinition).filter((index) => index !== void 0) ?? [];
638
+ const existingMappedIndex = existingIndexes.find((index) => index.mappedName !== void 0 && getPortableDatabaseIdentifierKey(index.mappedName) === getPortableDatabaseIdentifierKey(tableIndex.name));
639
+ if (existingMappedIndex) {
640
+ if (!prismaIndexMatches(existingMappedIndex, tableIndex)) throw new BetterAuthError(`Prisma index "${tableIndex.name}" on model "${modelName}" does not match the configured fields and uniqueness. Rename or replace the existing index, then generate the schema again.`);
641
+ continue;
642
+ }
643
+ const existingUnmappedIndex = existingIndexes.find((index) => index.mappedName === void 0 && prismaIndexMatches(index, tableIndex));
644
+ if (existingUnmappedIndex) {
645
+ existingUnmappedIndex.setMappedName(tableIndex.name);
646
+ continue;
647
+ }
648
+ builder.model(modelName).blockAttribute(`${attributeName}([${tableIndex.columns.join(", ")}], map: ${JSON.stringify(tableIndex.name)})`);
548
649
  }
549
650
  if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
550
651
  const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
@@ -0,0 +1,5 @@
1
+ import "dotenv/config";
2
+ //#region src/index.d.ts
3
+ declare let cliVersion: string;
4
+ //#endregion
5
+ export { cliVersion };
package/dist/index.mjs CHANGED
@@ -20,6 +20,7 @@ import { createPathsMatcher, getTsconfig, parseTsconfig } from "get-tsconfig";
20
20
  import fs$1 from "node:fs/promises";
21
21
  import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry";
22
22
  import { getAdapter } from "better-auth/db/adapter";
23
+ import { getDatabaseFieldIndexName, getDatabaseIndexStringLength, getPortableDatabaseIdentifierKey, resolveDatabaseSchemaIndexes } from "@better-auth/core/db/internal";
23
24
  import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
24
25
  import { initGetFieldName, initGetModelName } from "better-auth/adapters";
25
26
  import { getAuthTables } from "better-auth/db";
@@ -703,6 +704,11 @@ function addCloudflareVirtualModules(aliases) {
703
704
  * modules the real files depend on are not, which is why we stub `$app/*`
704
705
  * directly rather than resolving SvelteKit's on-disk files.
705
706
  *
707
+ * The one exception is `$app/env/{private,public}` (explicit environment
708
+ * variables): their exports are arbitrary names declared in the project's
709
+ * `src/env.ts`, so they cannot be enumerated. They are stubbed with a Proxy
710
+ * default export instead — see `createExplicitEnvModule`.
711
+ *
706
712
  * The authoritative export surfaces this mirrors:
707
713
  *
708
714
  * @see https://github.com/sveltejs/kit/tree/main/packages/kit/src/runtime/app
@@ -713,6 +719,9 @@ function addSvelteKitVirtualModules(aliases) {
713
719
  aliases["$env/dynamic/public"] = createStubModule(createDynamicEnvModule("public"));
714
720
  aliases["$env/static/private"] = createStubModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")));
715
721
  aliases["$env/static/public"] = createStubModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", "")));
722
+ const explicitEnvStub = createStubModule(createExplicitEnvModule());
723
+ aliases["$app/env/private"] = explicitEnvStub;
724
+ aliases["$app/env/public"] = explicitEnvStub;
716
725
  for (const [id, body] of Object.entries(appModuleStubs)) aliases[id] = createStubModule(body);
717
726
  }
718
727
  /**
@@ -824,6 +833,31 @@ export const env = new Proxy(
824
833
  },
825
834
  );`;
826
835
  }
836
+ /**
837
+ * Body for the explicit `$app/env/{private,public}` modules. Their exports are
838
+ * named after the vars declared in `src/env.ts`, which the CLI cannot know, so
839
+ * unlike the other stubs this cannot enumerate them. A Proxy exported as the
840
+ * *default* sidesteps that: jiti compiles `import { FOO } from "..."` to a
841
+ * member access on the (interop) default, which the Proxy answers from
842
+ * process.env. No prefix filtering — the public/private split is a `src/env.ts`
843
+ * config concern that does not affect schema generation.
844
+ */
845
+ function createExplicitEnvModule() {
846
+ return `
847
+ export default new Proxy(
848
+ {},
849
+ {
850
+ get: (_, key) =>
851
+ typeof key === "string" ? process.env[key] : undefined,
852
+ has: (_, key) => typeof key === "string" && key in process.env,
853
+ ownKeys: () => Object.keys(process.env),
854
+ getOwnPropertyDescriptor: (_, key) =>
855
+ typeof key === "string" && key in process.env
856
+ ? { value: process.env[key], enumerable: true, configurable: true }
857
+ : undefined,
858
+ },
859
+ );`;
860
+ }
827
861
  function filterPrivateEnv(publicPrefix, privatePrefix) {
828
862
  return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix))));
829
863
  }
@@ -831,7 +865,7 @@ function filterPublicEnv(publicPrefix, privatePrefix) {
831
865
  return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix))));
832
866
  }
833
867
  const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
834
- const reserved = new Set([
868
+ const reserved = /* @__PURE__ */ new Set([
835
869
  "do",
836
870
  "if",
837
871
  "in",
@@ -1051,7 +1085,7 @@ function resolveWithMatchers(specifier, matchers) {
1051
1085
  * a jiti-side preprocessor artifact observed in the AST; revisit on jiti
1052
1086
  * major version bumps (the regression suite catches a rename but not the why).
1053
1087
  */
1054
- const LOADER_IDENTIFIERS = new Set([
1088
+ const LOADER_IDENTIFIERS = /* @__PURE__ */ new Set([
1055
1089
  "require",
1056
1090
  "import",
1057
1091
  "jitiImport"
@@ -1133,7 +1167,27 @@ const resolveAuthModule = (mod) => {
1133
1167
  };
1134
1168
  const isServerOnlyError = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module");
1135
1169
  const SERVER_ONLY_HINT = `Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`;
1136
- async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
1170
+ /** Strips a source extension so paths can be compared across TS and JS inputs. */
1171
+ function withoutSourceExtension(filePath) {
1172
+ const ext = path.extname(filePath);
1173
+ return SOURCE_EXTENSIONS_SET.has(ext) ? filePath.slice(0, -ext.length) : filePath;
1174
+ }
1175
+ /**
1176
+ * Detects a first-run config import of the exact schema file being generated.
1177
+ * Only relative imports are eligible, so unrelated packages and missing files
1178
+ * continue to fail normally.
1179
+ *
1180
+ * @see https://github.com/better-auth/better-auth/issues/10136
1181
+ */
1182
+ function resolvesMissingOutputModule(error, configFilePath, resolvedOutputPath) {
1183
+ if (!error || typeof error !== "object" || error.code !== "MODULE_NOT_FOUND") return false;
1184
+ const specifier = ("message" in error && typeof error.message === "string" ? error.message : "").match(/Cannot find module ['"](\.\.?\/[^'"]+)['"]/)?.[1];
1185
+ if (!specifier) return false;
1186
+ const requireStack = "requireStack" in error && Array.isArray(error.requireStack) ? error.requireStack : [];
1187
+ const importedFrom = typeof requireStack[0] === "string" ? requireStack[0] : configFilePath;
1188
+ return withoutSourceExtension(path.resolve(path.dirname(importedFrom), specifier)) === withoutSourceExtension(resolvedOutputPath);
1189
+ }
1190
+ async function getConfig({ cwd, configPath, outputPath, shouldThrowOnError = false }) {
1137
1191
  const fail = (message, error) => {
1138
1192
  if (shouldThrowOnError) throw error instanceof Error ? error : new Error(message);
1139
1193
  const log = `[#better-auth]: ${message}`;
@@ -1141,13 +1195,30 @@ async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
1141
1195
  else console.error(log);
1142
1196
  process.exit(1);
1143
1197
  };
1144
- const load = (configFile) => loadConfig({
1145
- configFile,
1146
- dotenv: { fileName: [".env", ".env.local"] },
1147
- jitiOptions: jitiOptions(cwd),
1148
- resolveModule: resolveAuthModule,
1149
- cwd
1150
- });
1198
+ const resolvedOutputPath = outputPath ? path.resolve(cwd, outputPath) : void 0;
1199
+ const load = async (configFile) => {
1200
+ const loadOnce = () => loadConfig({
1201
+ configFile,
1202
+ dotenv: { fileName: [".env", ".env.local"] },
1203
+ jitiOptions: jitiOptions(cwd),
1204
+ resolveModule: resolveAuthModule,
1205
+ cwd
1206
+ });
1207
+ try {
1208
+ return await loadOnce();
1209
+ } catch (error) {
1210
+ const resolvedConfigPath = path.isAbsolute(configFile) ? configFile : path.resolve(cwd, configFile);
1211
+ if (!resolvedOutputPath || existsSync(resolvedOutputPath) || !resolvesMissingOutputModule(error, resolvedConfigPath, resolvedOutputPath)) throw error;
1212
+ await fs.promises.mkdir(path.dirname(resolvedOutputPath), { recursive: true });
1213
+ await fs.promises.writeFile(resolvedOutputPath, "");
1214
+ try {
1215
+ return await loadOnce();
1216
+ } catch (retryError) {
1217
+ await fs.promises.rm(resolvedOutputPath, { force: true }).catch(() => {});
1218
+ throw retryError;
1219
+ }
1220
+ }
1221
+ };
1151
1222
  try {
1152
1223
  if (configPath) {
1153
1224
  const resolvedPath = existsSync(configPath) ? configPath : path.join(cwd, configPath);
@@ -1288,17 +1359,30 @@ const createAdmin = new Command("create-admin").description("Create an initial a
1288
1359
  function convertToSnakeCase(str, camelCase) {
1289
1360
  return camelCase ? str : toSnakeCase(str);
1290
1361
  }
1362
+ function toValidIdentifier(str) {
1363
+ let result = str.replace(/[^a-zA-Z0-9_]/g, "").replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/^[0-9]/, "_$&");
1364
+ if (result.length > 0 && result[0].match(/[A-Z]/)) result = result[0].toLowerCase() + result.slice(1);
1365
+ return result || "schema";
1366
+ }
1291
1367
  const generateDrizzleSchema = async ({ options, file, adapter }) => {
1292
1368
  const tables = getAuthTables(options);
1293
1369
  const filePath = file || "./auth-schema.ts";
1294
1370
  const databaseType = adapter.options?.provider;
1371
+ const schemaName = adapter.options?.schemaName;
1295
1372
  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`);
1296
1373
  const fileExist = existsSync(filePath);
1297
1374
  let code = generateImport({
1298
1375
  databaseType,
1299
1376
  tables,
1300
- options
1377
+ options,
1378
+ schemaName
1301
1379
  });
1380
+ let schemaVarName;
1381
+ if (databaseType === "pg" && schemaName) {
1382
+ schemaVarName = `${toValidIdentifier(schemaName)}Schema`;
1383
+ if (schemaVarName === "pgSchema") schemaVarName = "pgCustomSchema";
1384
+ code += `\nconst ${schemaVarName} = pgSchema(${JSON.stringify(schemaName)});\n\n`;
1385
+ }
1302
1386
  const getModelName = initGetModelName({
1303
1387
  schema: tables,
1304
1388
  usePlural: adapter.options?.adapterConfig?.usePlural
@@ -1316,12 +1400,18 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1316
1400
  const customModelName = table.modelName || tableKey;
1317
1401
  return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
1318
1402
  });
1403
+ const resolvedIndexesByTable = resolveDatabaseSchemaIndexes(Object.keys(tables).filter((tableKey) => !isMigrationDisabled(tableKey)).map((tableKey) => ({
1404
+ fields: tables[tableKey].fields,
1405
+ indexes: tables[tableKey].indexes,
1406
+ tableName: getModelName(tableKey)
1407
+ })));
1319
1408
  for (const tableKey in tables) {
1320
1409
  const table = tables[tableKey];
1321
1410
  if (isMigrationDisabled(tableKey)) continue;
1322
1411
  const modelName = getModelName(tableKey);
1323
1412
  const fields = table.fields;
1324
- function getType(name, field) {
1413
+ const resolvedTableIndexes = resolvedIndexesByTable.get(modelName) ?? [];
1414
+ function getType(name, field, tableIndexStringLength) {
1325
1415
  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`);
1326
1416
  name = convertToSnakeCase(name, adapter.options?.camelCase);
1327
1417
  if (field.references?.field === "id") {
@@ -1347,7 +1437,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1347
1437
  string: {
1348
1438
  sqlite: `text('${name}')`,
1349
1439
  pg: `text('${name}')`,
1350
- 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}')`
1440
+ mysql: tableIndexStringLength ? `varchar('${name}', { length: ${tableIndexStringLength} })` : 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}')`
1351
1441
  },
1352
1442
  boolean: {
1353
1443
  sqlite: `integer('${name}', { mode: 'boolean' })`,
@@ -1396,30 +1486,35 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1396
1486
  const assignIndexes = (indexes) => {
1397
1487
  if (!indexes.length) return "";
1398
1488
  const code = [`, (table) => [`];
1399
- for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
1489
+ for (const index of indexes) code.push(` ${index.type}(${JSON.stringify(index.name)}).on(${index.on.map((fieldName) => `table.${fieldName}`).join(", ")}),`);
1400
1490
  code.push(`]`);
1401
1491
  return code.join("\n");
1402
1492
  };
1403
- const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
1493
+ for (const tableIndex of resolvedTableIndexes) indexes.push({
1494
+ type: tableIndex.unique ? "uniqueIndex" : "index",
1495
+ name: tableIndex.name,
1496
+ on: tableIndex.columns
1497
+ });
1498
+ const schema = `export const ${modelName} = ${databaseType === "pg" && schemaName && schemaVarName ? `${schemaVarName}.table` : `${databaseType}Table`}("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
1404
1499
  id: ${id},
1405
1500
  ${Object.keys(fields).map((field) => {
1406
1501
  const attr = fields[field];
1407
1502
  const fieldName = attr.fieldName || field;
1408
- let type = getType(fieldName, attr);
1503
+ let type = getType(fieldName, attr, databaseType === "mysql" ? getDatabaseIndexStringLength({
1504
+ columnName: fieldName,
1505
+ dialect: "mysql",
1506
+ fields,
1507
+ indexes: resolvedTableIndexes
1508
+ }) : void 0);
1409
1509
  if (attr.index && !attr.unique) indexes.push({
1410
1510
  type: "index",
1411
- name: `${modelName}_${fieldName}_idx`,
1412
- on: fieldName
1413
- });
1414
- else if (attr.index && attr.unique) indexes.push({
1415
- type: "uniqueIndex",
1416
- name: `${modelName}_${fieldName}_uidx`,
1417
- on: fieldName
1511
+ name: getDatabaseFieldIndexName(modelName, fieldName, false),
1512
+ on: [fieldName]
1418
1513
  });
1419
1514
  if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
1420
1515
  if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
1421
1516
  else type += `.defaultNow()`;
1422
- } else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
1517
+ } else if (typeof attr.defaultValue === "string") type += `.default(${JSON.stringify(attr.defaultValue)})`;
1423
1518
  else if (Array.isArray(attr.defaultValue)) {
1424
1519
  const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", ");
1425
1520
  type += `.default([${elements}])`;
@@ -1444,12 +1539,25 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1444
1539
  const modelName = getModelName(tableKey);
1445
1540
  const oneRelations = [];
1446
1541
  const manyRelations = [];
1447
- const manyRelationsSet = /* @__PURE__ */ new Set();
1448
1542
  const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
1543
+ const foreignFieldCounts = /* @__PURE__ */ new Map();
1544
+ for (const [_, field] of foreignFields) {
1545
+ const referencedModel = getModelName(field.references.model);
1546
+ foreignFieldCounts.set(referencedModel, (foreignFieldCounts.get(referencedModel) ?? 0) + 1);
1547
+ }
1548
+ const usedOneRelationKeys = /* @__PURE__ */ new Set();
1449
1549
  for (const [fieldName, field] of foreignFields) {
1450
1550
  const referencedModel = field.references.model;
1451
1551
  if (isMigrationDisabled(referencedModel)) continue;
1452
- const relationKey = getSingularModelName(referencedModel);
1552
+ const hasMultipleRelations = (foreignFieldCounts.get(getModelName(referencedModel)) ?? 0) > 1;
1553
+ let relationKey = hasMultipleRelations ? fieldName.replace(/Id$/, "") : getSingularModelName(referencedModel);
1554
+ if (usedOneRelationKeys.has(relationKey)) relationKey = fieldName;
1555
+ if (usedOneRelationKeys.has(relationKey)) {
1556
+ let suffix = 2;
1557
+ while (usedOneRelationKeys.has(`${relationKey}_${suffix}`)) suffix++;
1558
+ relationKey = `${relationKey}_${suffix}`;
1559
+ }
1560
+ usedOneRelationKeys.add(relationKey);
1453
1561
  const fieldRef = `${getModelName(tableKey)}.${getFieldName({
1454
1562
  model: tableKey,
1455
1563
  field: fieldName
@@ -1462,81 +1570,49 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1462
1570
  key: relationKey,
1463
1571
  model: getModelName(referencedModel),
1464
1572
  type: "one",
1573
+ relationName: hasMultipleRelations ? `${getModelName(tableKey)}_${fieldName}` : void 0,
1465
1574
  reference: {
1466
1575
  field: fieldRef,
1467
- references: referenceRef,
1468
- fieldName
1576
+ references: referenceRef
1469
1577
  }
1470
1578
  });
1471
1579
  }
1472
1580
  const otherModels = Object.entries(tables).filter(([modelName, otherTable]) => modelName !== tableKey && !otherTable.disableMigrations);
1473
- const modelRelationsMap = /* @__PURE__ */ new Map();
1474
1581
  for (const [modelName, otherTable] of otherModels) {
1475
1582
  const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
1476
1583
  if (foreignKeysPointingHere.length === 0) continue;
1477
- const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
1478
- const hasMany = foreignKeysPointingHere.some(([_, field]) => !field.unique);
1479
- modelRelationsMap.set(modelName, {
1480
- modelName,
1481
- hasUnique,
1482
- hasMany
1483
- });
1484
- }
1485
- for (const { modelName, hasMany } of modelRelationsMap.values()) {
1486
- const relationType = hasMany ? "many" : "one";
1487
- let relationKey = getModelName(modelName);
1488
- if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
1489
- if (!manyRelationsSet.has(relationKey)) {
1490
- manyRelationsSet.add(relationKey);
1584
+ for (const [fieldName, field] of foreignKeysPointingHere) {
1585
+ const relationType = field.unique ? "one" : "many";
1586
+ let relationKey = getModelName(modelName);
1587
+ if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
1588
+ const hasMultipleRelations = foreignKeysPointingHere.length > 1;
1589
+ if (hasMultipleRelations) relationKey = `${relationKey}By${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
1491
1590
  manyRelations.push({
1492
1591
  key: relationKey,
1493
1592
  model: getModelName(modelName),
1494
- type: relationType
1593
+ type: relationType,
1594
+ relationName: hasMultipleRelations ? `${getModelName(modelName)}_${fieldName}` : void 0
1495
1595
  });
1496
1596
  }
1497
1597
  }
1498
- const relationsByModel = /* @__PURE__ */ new Map();
1499
- for (const relation of oneRelations) if (relation.reference) {
1500
- const modelKey = relation.key;
1501
- if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
1502
- relationsByModel.get(modelKey).push(relation);
1503
- }
1504
- const duplicateRelations = [];
1505
- const singleRelations = [];
1506
- for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
1507
- else singleRelations.push(relations[0]);
1508
- for (const relation of duplicateRelations) if (relation.reference) {
1509
- const fieldName = relation.reference.fieldName;
1510
- const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
1511
- ${relation.key}: one(${relation.model}, {
1598
+ const hasForwardOne = oneRelations.length > 0;
1599
+ const hasReverseOne = manyRelations.some((relation) => relation.type === "one");
1600
+ const hasReverseMany = manyRelations.some((relation) => relation.type === "many");
1601
+ const hasOne = hasForwardOne || hasReverseOne;
1602
+ const hasMany = hasReverseMany;
1603
+ const renderOneRelation = (relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
1512
1604
  fields: [${relation.reference.field}],
1513
1605
  references: [${relation.reference.references}],
1514
- })
1515
- }))`;
1516
- relationsString += `\n${tableRelation}\n`;
1517
- }
1518
- const hasOne = singleRelations.length > 0;
1519
- const hasMany = manyRelations.length > 0;
1520
- if (hasOne && hasMany) {
1521
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
1522
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
1523
- fields: [${relation.reference.field}],
1524
- references: [${relation.reference.references}],
1525
- })` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
1526
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
1527
- }))`;
1528
- relationsString += `\n${tableRelation}\n`;
1529
- } else if (hasOne) {
1530
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
1531
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
1532
- fields: [${relation.reference.field}],
1533
- references: [${relation.reference.references}],
1534
- })` : "").filter((x) => x !== "").join(",\n ")}
1535
- }))`;
1536
- relationsString += `\n${tableRelation}\n`;
1537
- } else if (hasMany) {
1538
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
1539
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
1606
+ ${relation.relationName ? `relationName: "${relation.relationName}",` : ""}
1607
+ })` : "";
1608
+ const renderReverseRelation = ({ key, model, type, relationName }) => {
1609
+ return ` ${key}: ${type === "one" ? "one" : "many"}(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`;
1610
+ };
1611
+ if (hasOne || hasMany) {
1612
+ const helpers = [hasOne ? "one" : null, hasMany ? "many" : null].filter(Boolean).join(", ");
1613
+ const relationEntries = [...oneRelations.map(renderOneRelation).filter((x) => x !== ""), ...manyRelations.map(renderReverseRelation)].join(",\n ");
1614
+ const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ ${helpers} }) => ({
1615
+ ${relationEntries}
1540
1616
  }))`;
1541
1617
  relationsString += `\n${tableRelation}\n`;
1542
1618
  }
@@ -1548,7 +1624,7 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
1548
1624
  overwrite: fileExist
1549
1625
  };
1550
1626
  };
1551
- function generateImport({ databaseType, tables, options }) {
1627
+ function generateImport({ databaseType, tables, options, schemaName }) {
1552
1628
  const rootImports = ["relations"];
1553
1629
  const coreImports = [];
1554
1630
  let hasBigint = false;
@@ -1562,7 +1638,8 @@ function generateImport({ databaseType, tables, options }) {
1562
1638
  }
1563
1639
  const useNumberId = options.advanced?.database?.generateId === "serial";
1564
1640
  const useUUIDs = options.advanced?.database?.generateId === "uuid";
1565
- coreImports.push(`${databaseType}Table`);
1641
+ if (databaseType === "pg" && schemaName) coreImports.push("pgSchema");
1642
+ if (!(databaseType === "pg" && schemaName)) coreImports.push(`${databaseType}Table`);
1566
1643
  coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
1567
1644
  coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
1568
1645
  coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
@@ -1582,8 +1659,8 @@ function generateImport({ databaseType, tables, options }) {
1582
1659
  if (databaseType === "mysql") coreImports.push("json");
1583
1660
  }
1584
1661
  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");
1585
- const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
1586
- const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
1662
+ const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique) || (table.indexes?.some((index) => !index.unique) ?? false));
1663
+ const hasUniqueIndexes = Object.values(tables).some((table) => table.indexes?.some((index) => index.unique) ?? false);
1587
1664
  if (hasIndexes) coreImports.push("index");
1588
1665
  if (hasUniqueIndexes) coreImports.push("uniqueIndex");
1589
1666
  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`;
@@ -1702,11 +1779,58 @@ async function findMonorepoRoot(startDir) {
1702
1779
  }
1703
1780
  //#endregion
1704
1781
  //#region src/generators/prisma.ts
1782
+ function isRecord(value) {
1783
+ return typeof value === "object" && value !== null;
1784
+ }
1785
+ function parsePrismaStringLiteral(value) {
1786
+ if (typeof value !== "string") return void 0;
1787
+ try {
1788
+ const parsed = JSON.parse(value);
1789
+ return typeof parsed === "string" ? parsed : void 0;
1790
+ } catch {
1791
+ return;
1792
+ }
1793
+ }
1794
+ function getPrismaIndexDefinition(property) {
1795
+ if (!isRecord(property) || property.type !== "attribute" || property.kind !== "object" || property.name !== "index" && property.name !== "unique" || !Array.isArray(property.args)) return;
1796
+ const propertyArgs = property.args;
1797
+ let columns;
1798
+ let mappedName;
1799
+ let validFullColumns = false;
1800
+ for (const argument of propertyArgs) {
1801
+ if (!isRecord(argument) || !isRecord(argument.value)) continue;
1802
+ const value = argument.value;
1803
+ if (value.type === "array" && Array.isArray(value.args) && value.args.every((column) => typeof column === "string")) {
1804
+ columns = value.args.map((column) => String(column));
1805
+ validFullColumns = true;
1806
+ } else if (value.type === "keyValue" && value.key === "map") mappedName = parsePrismaStringLiteral(value.value);
1807
+ }
1808
+ return {
1809
+ columns: columns ?? [],
1810
+ mappedName,
1811
+ setMappedName(name) {
1812
+ propertyArgs.push({
1813
+ type: "attributeArgument",
1814
+ value: {
1815
+ type: "keyValue",
1816
+ key: "map",
1817
+ value: JSON.stringify(name)
1818
+ }
1819
+ });
1820
+ },
1821
+ unique: property.name === "unique",
1822
+ validFullColumns
1823
+ };
1824
+ }
1825
+ function prismaIndexMatches(existing, configured) {
1826
+ return existing.validFullColumns && existing.unique === (configured.unique ?? false) && existing.columns.length === configured.columns.length && existing.columns.every((column, position) => column === configured.columns[position]);
1827
+ }
1705
1828
  const generatePrismaSchema = async ({ adapter, options, file }) => {
1706
1829
  const provider = adapter.options?.provider || "postgresql";
1707
1830
  const tables = getAuthTables(options);
1708
1831
  const filePath = file || "./prisma/schema.prisma";
1709
- const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
1832
+ const resolvedFilePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
1833
+ const schemaPrismaExist = existsSync(resolvedFilePath);
1710
1834
  const getModelName = initGetModelName({
1711
1835
  schema: getAuthTables(options),
1712
1836
  usePlural: adapter.options?.adapterConfig?.usePlural
@@ -1720,8 +1844,16 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
1720
1844
  const customModelName = table.modelName || tableKey;
1721
1845
  return model === tableKey || model === customModelName || model === getModelName(tableKey) || model === getModelName(customModelName);
1722
1846
  });
1847
+ const resolvedIndexesByTable = resolveDatabaseSchemaIndexes(Object.keys(tables).filter((tableKey) => !isMigrationDisabled(tableKey)).map((tableKey) => {
1848
+ const customModelName = tables[tableKey]?.modelName || tableKey;
1849
+ return {
1850
+ fields: tables[tableKey].fields,
1851
+ indexes: tables[tableKey].indexes,
1852
+ tableName: getModelName(customModelName)
1853
+ };
1854
+ }));
1723
1855
  let schemaPrisma = "";
1724
- if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8");
1856
+ if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(resolvedFilePath, "utf-8");
1725
1857
  else schemaPrisma = getNewPrisma(provider, process.cwd());
1726
1858
  const prismaVersion = getPrismaVersion(process.cwd());
1727
1859
  if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
@@ -1772,8 +1904,10 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
1772
1904
  const customModelName = tables[table]?.modelName || table;
1773
1905
  const modelName = capitalizeFirstLetter(getModelName(customModelName));
1774
1906
  const fields = tables[table]?.fields;
1907
+ const resolvedTableIndexes = resolvedIndexesByTable.get(getModelName(customModelName)) ?? [];
1775
1908
  function getType({ isBigint, isOptional, type }) {
1776
1909
  if (type === "string") return isOptional ? "String?" : "String";
1910
+ if (Array.isArray(type)) return isOptional ? "String?" : "String";
1777
1911
  if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
1778
1912
  if (type === "number") return isOptional ? "Int?" : "Int";
1779
1913
  if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
@@ -1839,6 +1973,18 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
1839
1973
  isAlreadyExist.array = fieldTypeParts.isArray || void 0;
1840
1974
  }
1841
1975
  }
1976
+ if (provider === "mysql" && (attr.type === "string" || Array.isArray(attr.type)) && typeof isAlreadyExist.fieldType === "string" && getFieldTypeParts(isAlreadyExist.fieldType).fieldType === "String") {
1977
+ const tableIndexStringLength = getDatabaseIndexStringLength({
1978
+ columnName: fieldName,
1979
+ dialect: "mysql",
1980
+ fields: fields ?? {},
1981
+ indexes: resolvedTableIndexes
1982
+ });
1983
+ if (tableIndexStringLength) {
1984
+ isAlreadyExist.attributes = isAlreadyExist.attributes?.filter((attribute) => attribute.group !== "db");
1985
+ builder.model(modelName).field(fieldName).attribute(`db.VarChar(${tableIndexStringLength})`);
1986
+ }
1987
+ }
1842
1988
  continue;
1843
1989
  }
1844
1990
  }
@@ -1906,7 +2052,31 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
1906
2052
  })}], onDelete: ${action})`;
1907
2053
  builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField);
1908
2054
  }
1909
- if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
2055
+ if (provider === "mysql" && (attr.type === "string" || Array.isArray(attr.type))) {
2056
+ const tableIndexStringLength = getDatabaseIndexStringLength({
2057
+ columnName: fieldName,
2058
+ dialect: "mysql",
2059
+ fields: fields ?? {},
2060
+ indexes: resolvedTableIndexes
2061
+ });
2062
+ const nativeType = tableIndexStringLength ? `db.VarChar(${tableIndexStringLength})` : !attr.unique && !attr.references ? "db.Text" : void 0;
2063
+ if (nativeType) builder.model(modelName).field(fieldName).attribute(nativeType);
2064
+ }
2065
+ }
2066
+ for (const tableIndex of resolvedTableIndexes) {
2067
+ const attributeName = tableIndex.unique ? "unique" : "index";
2068
+ const existingIndexes = prismaModel?.properties.map(getPrismaIndexDefinition).filter((index) => index !== void 0) ?? [];
2069
+ const existingMappedIndex = existingIndexes.find((index) => index.mappedName !== void 0 && getPortableDatabaseIdentifierKey(index.mappedName) === getPortableDatabaseIdentifierKey(tableIndex.name));
2070
+ if (existingMappedIndex) {
2071
+ if (!prismaIndexMatches(existingMappedIndex, tableIndex)) throw new BetterAuthError(`Prisma index "${tableIndex.name}" on model "${modelName}" does not match the configured fields and uniqueness. Rename or replace the existing index, then generate the schema again.`);
2072
+ continue;
2073
+ }
2074
+ const existingUnmappedIndex = existingIndexes.find((index) => index.mappedName === void 0 && prismaIndexMatches(index, tableIndex));
2075
+ if (existingUnmappedIndex) {
2076
+ existingUnmappedIndex.setMappedName(tableIndex.name);
2077
+ continue;
2078
+ }
2079
+ builder.model(modelName).blockAttribute(`${attributeName}([${tableIndex.columns.join(", ")}], map: ${JSON.stringify(tableIndex.name)})`);
1910
2080
  }
1911
2081
  if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
1912
2082
  const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
@@ -2066,18 +2236,33 @@ async function generateAction(opts) {
2066
2236
  console.error(`The directory "${cwd}" does not exist.`);
2067
2237
  process.exit(1);
2068
2238
  }
2239
+ let outputIsDirectory = false;
2240
+ if (options.output) try {
2241
+ outputIsDirectory = (await fs$1.stat(path.resolve(cwd, options.output))).isDirectory();
2242
+ } catch {}
2243
+ const configOutputPath = options.output && !outputIsDirectory ? options.output : void 0;
2244
+ const resolvedConfigOutputPath = configOutputPath ? path.resolve(cwd, configOutputPath) : void 0;
2245
+ const outputExistedBefore = resolvedConfigOutputPath ? existsSync(resolvedConfigOutputPath) : true;
2246
+ const removeGeneratedStub = async () => {
2247
+ if (!resolvedConfigOutputPath || outputExistedBefore) return;
2248
+ await fs$1.rm(resolvedConfigOutputPath, { force: true }).catch(() => {});
2249
+ };
2069
2250
  const config = await getConfig({
2070
2251
  cwd,
2071
- configPath: options.config
2252
+ configPath: options.config,
2253
+ outputPath: configOutputPath
2072
2254
  });
2073
2255
  if (!config) {
2256
+ await removeGeneratedStub();
2074
2257
  console.error("No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.");
2075
2258
  return;
2076
2259
  }
2260
+ await removeGeneratedStub();
2077
2261
  let adapter;
2078
2262
  if (options.adapter) adapter = createMockAdapter$1(options.adapter, options.dialect);
2079
- else adapter = await getAdapter(config).catch((e) => {
2263
+ else adapter = await getAdapter(config).catch(async (e) => {
2080
2264
  console.error(e.message);
2265
+ await removeGeneratedStub();
2081
2266
  process.exit(1);
2082
2267
  });
2083
2268
  options.output = await resolveSchemaOutputPath({
@@ -2085,14 +2270,16 @@ async function generateAction(opts) {
2085
2270
  output: options.output,
2086
2271
  adapterId: adapter.id
2087
2272
  });
2273
+ const resolvedOutputPath = options.output ? path.resolve(cwd, options.output) : void 0;
2088
2274
  const spinner = yoctoSpinner({ text: "preparing schema..." }).start();
2089
2275
  const schema = await generateSchema({
2090
2276
  adapter,
2091
- file: options.output,
2277
+ file: resolvedOutputPath ?? options.output,
2092
2278
  options: config
2093
2279
  });
2094
2280
  spinner.stop();
2095
2281
  if (!schema.code) {
2282
+ await removeGeneratedStub();
2096
2283
  console.log("Your schema is already up to date.");
2097
2284
  try {
2098
2285
  await (await createTelemetry(config)).publish({
@@ -2116,9 +2303,10 @@ async function generateAction(opts) {
2116
2303
  message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(`${schema.overwrite ? "overwrite" : "append"}`)} the schema to the file?`
2117
2304
  })).confirm;
2118
2305
  if (confirm) {
2119
- if (!existsSync(path.join(cwd, schema.fileName))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
2120
- if (schema.overwrite) await fs$1.writeFile(path.join(cwd, schema.fileName), schema.code);
2121
- else await fs$1.appendFile(path.join(cwd, schema.fileName), schema.code);
2306
+ const schemaPath = path.isAbsolute(schema.fileName) ? schema.fileName : path.join(cwd, schema.fileName);
2307
+ if (!existsSync(schemaPath)) await fs$1.mkdir(path.dirname(schemaPath), { recursive: true });
2308
+ if (schema.overwrite) await fs$1.writeFile(schemaPath, schema.code);
2309
+ else await fs$1.appendFile(schemaPath, schema.code);
2122
2310
  console.log(`🚀 Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`);
2123
2311
  try {
2124
2312
  await (await createTelemetry(config)).publish({
@@ -2132,6 +2320,7 @@ async function generateAction(opts) {
2132
2320
  process.exit(0);
2133
2321
  } else {
2134
2322
  console.error("Schema generation aborted.");
2323
+ await removeGeneratedStub();
2135
2324
  try {
2136
2325
  await (await createTelemetry(config)).publish({
2137
2326
  type: "cli_generate",
@@ -2156,6 +2345,7 @@ async function generateAction(opts) {
2156
2345
  })).confirm;
2157
2346
  if (!confirm) {
2158
2347
  console.error("Schema generation aborted.");
2348
+ await removeGeneratedStub();
2159
2349
  try {
2160
2350
  await (await createTelemetry(config)).publish({
2161
2351
  type: "cli_generate",
@@ -2167,10 +2357,9 @@ async function generateAction(opts) {
2167
2357
  } catch {}
2168
2358
  process.exit(1);
2169
2359
  }
2170
- if (!options.output) {
2171
- if (!existsSync(path.dirname(path.join(cwd, schema.fileName)))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
2172
- }
2173
- await fs$1.writeFile(options.output || path.join(cwd, schema.fileName), schema.code);
2360
+ const writePath = resolvedOutputPath ?? path.join(cwd, schema.fileName);
2361
+ if (!existsSync(path.dirname(writePath))) await fs$1.mkdir(path.dirname(writePath), { recursive: true });
2362
+ await fs$1.writeFile(writePath, schema.code);
2174
2363
  console.log(`🚀 Schema was generated successfully!`);
2175
2364
  try {
2176
2365
  await (await createTelemetry(config)).publish({
@@ -4762,26 +4951,6 @@ const tempPluginsConfig = {
4762
4951
  }]
4763
4952
  }
4764
4953
  },
4765
- scim: {
4766
- displayName: "SCIM",
4767
- dependencies: ["@better-auth/scim"],
4768
- auth: {
4769
- function: "scim",
4770
- imports: [{
4771
- path: "@better-auth/scim",
4772
- imports: [createImport({ name: "scim" })],
4773
- isNamedImport: false
4774
- }]
4775
- },
4776
- authClient: {
4777
- function: "scimClient",
4778
- imports: [{
4779
- path: "@better-auth/scim/client",
4780
- imports: [createImport({ name: "scimClient" })],
4781
- isNamedImport: false
4782
- }]
4783
- }
4784
- },
4785
4954
  sso: {
4786
4955
  displayName: "SSO",
4787
4956
  dependencies: ["@better-auth/sso"],
@@ -5194,11 +5363,12 @@ const getAuthPluginsCode = async ({ plugins, options = {}, installDependency })
5194
5363
  const getArguments = await getArgumentsPrompt(options, plugins, "auth");
5195
5364
  const pluginsCode = [];
5196
5365
  for (const plugin of plugins) {
5197
- const args = convertArgumentsCodeToStringArray(await buildArgumentsCode(plugin.auth.arguments, getArguments, plugin.auth.function));
5366
+ const argumentsCode = await buildArgumentsCode(plugin.auth.arguments, getArguments, plugin.auth.function);
5367
+ const args = convertArgumentsCodeToStringArray(argumentsCode);
5198
5368
  removeTrailingUndefined(args);
5199
5369
  pluginsCode.push(`${plugin.auth.function}(${args.join(", ")})`);
5200
- const dependencies = new Set([...plugin.dependencies || [], ...plugin.auth.dependencies || []]);
5201
- const devDependencies = new Set([...plugin.devDependencies || [], ...plugin.auth.devDependencies || []]);
5370
+ const dependencies = /* @__PURE__ */ new Set([...plugin.dependencies || [], ...plugin.auth.dependencies || []]);
5371
+ const devDependencies = /* @__PURE__ */ new Set([...plugin.devDependencies || [], ...plugin.auth.devDependencies || []]);
5202
5372
  if (dependencies.size > 0) await installDependency([...dependencies]);
5203
5373
  if (devDependencies.size > 0) await installDependency([...devDependencies], "dev");
5204
5374
  }
@@ -5212,11 +5382,12 @@ const getAuthClientPluginsCode = async ({ plugins, options = {}, installDependen
5212
5382
  const pluginsCode = [];
5213
5383
  for (const plugin of pluginsWithClient) {
5214
5384
  if (!plugin.authClient) continue;
5215
- const args = convertArgumentsCodeToStringArray(await buildArgumentsCode(plugin.authClient.arguments, getArguments, plugin.authClient.function));
5385
+ const argumentsCode = await buildArgumentsCode(plugin.authClient.arguments, getArguments, plugin.authClient.function);
5386
+ const args = convertArgumentsCodeToStringArray(argumentsCode);
5216
5387
  removeTrailingUndefined(args);
5217
5388
  pluginsCode.push(`${plugin.authClient.function}(${args.join(", ")})`);
5218
- const dependencies = new Set([...plugin.dependencies || [], ...plugin.authClient.dependencies || []]);
5219
- const devDependencies = new Set([...plugin.devDependencies || [], ...plugin.authClient.devDependencies || []]);
5389
+ const dependencies = /* @__PURE__ */ new Set([...plugin.dependencies || [], ...plugin.authClient.dependencies || []]);
5390
+ const devDependencies = /* @__PURE__ */ new Set([...plugin.devDependencies || [], ...plugin.authClient.devDependencies || []]);
5220
5391
  if (dependencies.size > 0) await installDependency([...dependencies]);
5221
5392
  if (devDependencies.size > 0) await installDependency([...devDependencies], "dev");
5222
5393
  }
@@ -6389,9 +6560,10 @@ export const auth = betterAuth({
6389
6560
  }
6390
6561
  databaseChoice = dbChoice || null;
6391
6562
  if (databaseChoice === "yes") {
6563
+ const availableORMs = getAvailableORMs();
6392
6564
  const selectedOption = await select({
6393
6565
  message: `Select the database you want to use:`,
6394
- options: getAvailableORMs().map((opt) => ({
6566
+ options: availableORMs.map((opt) => ({
6395
6567
  value: opt.adapter || opt.value,
6396
6568
  label: opt.label
6397
6569
  }))
@@ -6425,9 +6597,10 @@ export const auth = betterAuth({
6425
6597
  database = sqliteVariants;
6426
6598
  } else if (isDirectAdapter(selectedOption)) database = selectedOption;
6427
6599
  else {
6600
+ const availableDialects = getDialectsForORM(selectedOption);
6428
6601
  const selectedDialect = await select({
6429
6602
  message: `Select the database dialect:`,
6430
- options: getDialectsForORM(selectedOption).map((d) => ({
6603
+ options: availableDialects.map((d) => ({
6431
6604
  value: d.adapter,
6432
6605
  label: d.label
6433
6606
  }))
@@ -6445,7 +6618,7 @@ export const auth = betterAuth({
6445
6618
  const { shouldInstallDeps } = await prompts({
6446
6619
  type: "confirm",
6447
6620
  name: "shouldInstallDeps",
6448
- message: `Would you like to install the following dependencies: ${[...new Set([...databaseConfig.dependencies, ...databaseConfig.devDependencies || []])].map((x) => chalk.cyan(x)).join(", ")}?`,
6621
+ message: `Would you like to install the following dependencies: ${[.../* @__PURE__ */ new Set([...databaseConfig.dependencies, ...databaseConfig.devDependencies || []])].map((x) => chalk.cyan(x)).join(", ")}?`,
6449
6622
  initial: true
6450
6623
  });
6451
6624
  if (isCancel(shouldInstallDeps)) {
@@ -7162,8 +7335,8 @@ async function migrateAction(opts) {
7162
7335
  process.exit(1);
7163
7336
  }
7164
7337
  const spinner = yoctoSpinner({ text: "preparing migration..." }).start();
7165
- const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config);
7166
- if (!toBeAdded.length && !toBeCreated.length) {
7338
+ const { toBeAdded, toBeAddedIndexes, toBeCreated, runMigrations } = await getMigrations(config);
7339
+ if (!toBeAdded.length && !toBeAddedIndexes.length && !toBeCreated.length) {
7167
7340
  spinner.stop();
7168
7341
  console.log("🚀 No migrations needed.");
7169
7342
  try {
@@ -7180,6 +7353,7 @@ async function migrateAction(opts) {
7180
7353
  spinner.stop();
7181
7354
  console.log(`🔑 The migration will affect the following:`);
7182
7355
  for (const table of [...toBeCreated, ...toBeAdded]) console.log("->", chalk.magenta(Object.keys(table.fields).join(", ")), chalk.white("fields on"), chalk.yellow(`${table.table}`), chalk.white("table."));
7356
+ for (const { index, table } of toBeAddedIndexes) console.log("->", chalk.magenta(index.columns.join(", ")), chalk.white(index.unique ? "fields in a unique index on" : "fields indexed on"), chalk.yellow(table), chalk.white("table."));
7183
7357
  if (options.y) {
7184
7358
  console.warn("WARNING: --y is deprecated. Consider -y or --yes");
7185
7359
  options.yes = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth",
3
- "version": "1.7.0-rc.0",
3
+ "version": "1.7.0-rc.2",
4
4
  "description": "The CLI for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "@babel/preset-react": "^7.28.5",
51
51
  "@babel/preset-typescript": "^7.28.5",
52
52
  "@better-auth/utils": "0.4.2",
53
- "@clack/prompts": "^1.5.1",
53
+ "@clack/prompts": "^1.6.0",
54
54
  "@mrleebo/prisma-ast": "^0.16.0",
55
55
  "c12": "^4.0.0-beta.5",
56
56
  "chalk": "^5.6.2",
@@ -64,9 +64,9 @@
64
64
  "semver": "^7.8.4",
65
65
  "yocto-spinner": "^1.2.0",
66
66
  "zod": "^4.3.6",
67
- "@better-auth/core": "1.7.0-rc.0",
68
- "@better-auth/telemetry": "1.7.0-rc.0",
69
- "better-auth": "1.7.0-rc.0"
67
+ "@better-auth/core": "1.7.0-rc.2",
68
+ "@better-auth/telemetry": "1.7.0-rc.2",
69
+ "better-auth": "1.7.0-rc.2"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/better-sqlite3": "^7.6.13",
@@ -74,11 +74,11 @@
74
74
  "@types/semver": "^7.7.1",
75
75
  "better-sqlite3": "^12.11.1",
76
76
  "memfs": "^4.56.10",
77
- "tsdown": "0.21.1",
77
+ "tsdown": "0.22.7",
78
78
  "tsx": "^4.21.0",
79
79
  "type-fest": "^5.7.0",
80
- "typescript": "^5.9.3",
81
- "@better-auth/passkey": "1.7.0-rc.0"
80
+ "typescript": "^6.0.3",
81
+ "@better-auth/passkey": "1.7.0-rc.2"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsdown",