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 +0 -1
- package/dist/api.mjs +182 -81
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +306 -132
- package/package.json +8 -8
package/dist/api.d.mts
CHANGED
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
|
-
|
|
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}(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
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(
|
|
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 (
|
|
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);
|
package/dist/index.d.mts
ADDED
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
|
-
|
|
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
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
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}(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
|
|
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(
|
|
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) =>
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
2120
|
-
if (
|
|
2121
|
-
|
|
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
|
-
|
|
2171
|
-
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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: ${[
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
-
"@better-auth/telemetry": "1.7.0-rc.
|
|
69
|
-
"better-auth": "1.7.0-rc.
|
|
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.
|
|
77
|
+
"tsdown": "0.22.7",
|
|
78
78
|
"tsx": "^4.21.0",
|
|
79
79
|
"type-fest": "^5.7.0",
|
|
80
|
-
"typescript": "^
|
|
81
|
-
"@better-auth/passkey": "1.7.0-rc.
|
|
80
|
+
"typescript": "^6.0.3",
|
|
81
|
+
"@better-auth/passkey": "1.7.0-rc.2"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|