auth
Version:
The CLI for Better Auth
585 lines (582 loc) • 29.8 kB
JavaScript
import { existsSync, readFileSync } from "node:fs";
import { capitalizeFirstLetter, toSnakeCase } from "@better-auth/core/utils/string";
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
import { getAuthTables } from "better-auth/db";
import prettier from "prettier";
import { getMigrations } from "better-auth/db/migration";
import fs from "node:fs/promises";
import path from "node:path";
import { produceSchema } from "@mrleebo/prisma-ast";
//#region src/generators/drizzle.ts
function convertToSnakeCase(str, camelCase) {
return camelCase ? str : toSnakeCase(str);
}
const generateDrizzleSchema = async ({ options, file, adapter }) => {
const tables = getAuthTables(options);
const filePath = file || "./auth-schema.ts";
const databaseType = adapter.options?.provider;
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`);
const fileExist = existsSync(filePath);
let code = generateImport({
databaseType,
tables,
options
});
const getModelName = initGetModelName({
schema: tables,
usePlural: adapter.options?.adapterConfig?.usePlural
});
const getFieldName = initGetFieldName({
schema: tables,
usePlural: adapter.options?.adapterConfig?.usePlural
});
for (const tableKey in tables) {
const table = tables[tableKey];
if (table.disableMigrations) continue;
const modelName = getModelName(tableKey);
const fields = table.fields;
function getType(name, field) {
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`);
name = convertToSnakeCase(name, adapter.options?.camelCase);
if (field.references?.field === "id") {
const useNumberId = options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (useNumberId) if (databaseType === "pg") return `integer('${name}')`;
else if (databaseType === "mysql") return `int('${name}')`;
else return `integer('${name}')`;
if (useUUIDs && databaseType === "pg") return `uuid('${name}')`;
if (field.references.field) {
if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`;
}
return `text('${name}')`;
}
const type = field.type;
if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
}[databaseType];
else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
const dbTypeMap = {
string: {
sqlite: `text('${name}')`,
pg: `text('${name}')`,
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}')`
},
boolean: {
sqlite: `integer('${name}', { mode: 'boolean' })`,
pg: `boolean('${name}')`,
mysql: `boolean('${name}')`
},
number: {
sqlite: `integer('${name}')`,
pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
},
date: {
sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
pg: `timestamp('${name}')`,
mysql: `timestamp('${name}', { fsp: 3 })`
},
"number[]": {
sqlite: `text('${name}', { mode: "json" })`,
pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
mysql: `text('${name}', { mode: 'json' })`
},
"string[]": {
sqlite: `text('${name}', { mode: "json" })`,
pg: `text('${name}').array()`,
mysql: `text('${name}', { mode: "json" })`
},
json: {
sqlite: `text('${name}', { mode: "json" })`,
pg: `jsonb('${name}')`,
mysql: `json('${name}', { mode: "json" })`
}
}[type];
if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
return dbTypeMap[databaseType];
}
let id = "";
const useNumberId = options.advanced?.database?.generateId === "serial";
if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`;
else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
else id = `int("id").autoincrement().primaryKey()`;
else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
else if (databaseType === "pg") id = `text('id').primaryKey()`;
else id = `text('id').primaryKey()`;
const indexes = [];
const assignIndexes = (indexes) => {
if (!indexes.length) return "";
const code = [`, (table) => [`];
for (const index of indexes) code.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
code.push(`]`);
return code.join("\n");
};
const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
id: ${id},
${Object.keys(fields).map((field) => {
const attr = fields[field];
const fieldName = attr.fieldName || field;
let type = getType(fieldName, attr);
if (attr.index && !attr.unique) indexes.push({
type: "index",
name: `${modelName}_${fieldName}_idx`,
on: fieldName
});
if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
else type += `.defaultNow()`;
} else if (typeof attr.defaultValue === "string") type += `.default(${JSON.stringify(attr.defaultValue)})`;
else if (Array.isArray(attr.defaultValue)) {
const elements = attr.defaultValue.map((value) => JSON.stringify(value)).join(", ");
type += `.default([${elements}])`;
} else if (typeof attr.defaultValue === "object" && attr.defaultValue !== null) type += `.default(${JSON.stringify(attr.defaultValue)})`;
else type += `.default(${attr.defaultValue})`;
if (attr.onUpdate && attr.type === "date") {
if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
}
return `${fieldName}: ${type}${attr.required !== false ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
model: attr.references.model,
field: attr.references.field
})}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
}).join(",\n ")}
}${assignIndexes(indexes)});`;
code += `\n${schema}\n`;
}
let relationsString = "";
for (const tableKey in tables) {
const table = tables[tableKey];
if (table.disableMigrations) continue;
const modelName = getModelName(tableKey);
const oneRelations = [];
const manyRelations = [];
const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
const foreignFieldCounts = /* @__PURE__ */ new Map();
for (const [_, field] of foreignFields) {
const referencedModel = getModelName(field.references.model);
foreignFieldCounts.set(referencedModel, (foreignFieldCounts.get(referencedModel) ?? 0) + 1);
}
const usedOneRelationKeys = /* @__PURE__ */ new Set();
for (const [fieldName, field] of foreignFields) {
const referencedModel = field.references.model;
const hasMultipleRelations = (foreignFieldCounts.get(getModelName(referencedModel)) ?? 0) > 1;
let relationKey = hasMultipleRelations ? fieldName.replace(/Id$/, "") : getModelName(referencedModel);
if (usedOneRelationKeys.has(relationKey)) relationKey = fieldName;
if (usedOneRelationKeys.has(relationKey)) {
let suffix = 2;
while (usedOneRelationKeys.has(`${relationKey}_${suffix}`)) suffix++;
relationKey = `${relationKey}_${suffix}`;
}
usedOneRelationKeys.add(relationKey);
const fieldRef = `${getModelName(tableKey)}.${getFieldName({
model: tableKey,
field: fieldName
})}`;
const referenceRef = `${getModelName(referencedModel)}.${getFieldName({
model: referencedModel,
field: field.references.field || "id"
})}`;
oneRelations.push({
key: relationKey,
model: getModelName(referencedModel),
type: "one",
relationName: hasMultipleRelations ? `${getModelName(tableKey)}_${fieldName}` : void 0,
reference: {
field: fieldRef,
references: referenceRef
}
});
}
const otherModels = Object.entries(tables).filter(([modelName]) => modelName !== tableKey);
for (const [modelName, otherTable] of otherModels) {
const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
if (foreignKeysPointingHere.length === 0) continue;
for (const [fieldName, field] of foreignKeysPointingHere) {
const relationType = field.unique ? "one" : "many";
let relationKey = getModelName(modelName);
if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
const hasMultipleRelations = foreignKeysPointingHere.length > 1;
if (hasMultipleRelations) relationKey = `${relationKey}By${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
manyRelations.push({
key: relationKey,
model: getModelName(modelName),
type: relationType,
relationName: hasMultipleRelations ? `${getModelName(modelName)}_${fieldName}` : void 0
});
}
}
const hasForwardOne = oneRelations.length > 0;
const hasReverseOne = manyRelations.some((relation) => relation.type === "one");
const hasReverseMany = manyRelations.some((relation) => relation.type === "many");
const hasOne = hasForwardOne || hasReverseOne;
const hasMany = hasReverseMany;
const renderOneRelation = (relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
fields: [${relation.reference.field}],
references: [${relation.reference.references}],
${relation.relationName ? `relationName: "${relation.relationName}",` : ""}
})` : "";
const renderReverseRelation = ({ key, model, type, relationName }) => {
return ` ${key}: ${type === "one" ? "one" : "many"}(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`;
};
if (hasOne || hasMany) {
const helpers = [hasOne ? "one" : null, hasMany ? "many" : null].filter(Boolean).join(", ");
const relationEntries = [...oneRelations.map(renderOneRelation).filter((x) => x !== ""), ...manyRelations.map(renderReverseRelation)].join(",\n ");
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ ${helpers} }) => ({
${relationEntries}
}))`;
relationsString += `\n${tableRelation}\n`;
}
}
code += `\n${relationsString}`;
return {
code: await prettier.format(code, { parser: "typescript" }),
fileName: filePath,
overwrite: fileExist
};
};
function generateImport({ databaseType, tables, options }) {
const rootImports = ["relations"];
const coreImports = [];
let hasBigint = false;
let hasJson = false;
for (const table of Object.values(tables)) {
for (const field of Object.values(table.fields)) {
if (field.bigint) hasBigint = true;
if (field.type === "json") hasJson = true;
}
if (hasJson && hasBigint) break;
}
const useNumberId = options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
coreImports.push(`${databaseType}Table`);
coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
if (databaseType === "mysql") {
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
if (useNumberId || hasNonBigintNumber) coreImports.push("int");
if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
} else if (databaseType === "pg") {
if (useUUIDs) rootImports.push("sql");
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id"));
if (hasNonBigintNumber || options.advanced?.database?.generateId === "serial" && hasFkToId) coreImports.push("integer");
} else coreImports.push("integer");
if (databaseType === "pg" && useUUIDs) coreImports.push("uuid");
if (hasJson) {
if (databaseType === "pg") coreImports.push("jsonb");
if (databaseType === "mysql") coreImports.push("json");
}
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");
if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique))) coreImports.push("index");
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`;
}
//#endregion
//#region src/generators/kysely.ts
const generateKyselySchema = async ({ options, file }) => {
const { compileMigrations } = await getMigrations(options);
const migrations = await compileMigrations();
return {
code: migrations.trim() === ";" ? "" : migrations,
fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
};
};
//#endregion
//#region src/utils/get-package-info.ts
function getPackageInfo(cwd) {
const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
}
function getPrismaVersion(cwd) {
try {
const packageInfo = getPackageInfo(cwd);
const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"];
if (!prismaVersion) return null;
const match = prismaVersion.match(/(\d+)/);
return match ? parseInt(match[1], 10) : null;
} catch {
return null;
}
}
//#endregion
//#region src/generators/prisma.ts
const generatePrismaSchema = async ({ adapter, options, file }) => {
const provider = adapter.options?.provider || "postgresql";
const tables = getAuthTables(options);
const filePath = file || "./prisma/schema.prisma";
const resolvedFilePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
const schemaPrismaExist = existsSync(resolvedFilePath);
const getModelName = initGetModelName({
schema: getAuthTables(options),
usePlural: adapter.options?.adapterConfig?.usePlural
});
const getFieldName = initGetFieldName({
schema: getAuthTables(options),
usePlural: false
});
let schemaPrisma = "";
if (schemaPrismaExist) schemaPrisma = await fs.readFile(resolvedFilePath, "utf-8");
else schemaPrisma = getNewPrisma(provider, process.cwd());
const prismaVersion = getPrismaVersion(process.cwd());
if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
const generator = builder.findByType("generator", { name: "client" });
if (generator && generator.properties) {
const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider");
if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\"";
}
const datasource = builder.findByType("datasource", { name: "db" });
if (datasource && datasource.properties) {
const urlIndex = datasource.properties.findIndex((prop) => prop.type === "assignment" && prop.key === "url");
if (urlIndex !== -1) datasource.properties.splice(urlIndex, 1);
}
});
const manyToManyRelations = /* @__PURE__ */ new Map();
for (const table in tables) {
const fields = tables[table]?.fields;
for (const field in fields) {
const attr = fields[field];
if (attr.references) {
const referencedOriginalModel = attr.references.model;
const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel));
if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
}
}
}
const indexedFields = /* @__PURE__ */ new Map();
for (const table in tables) {
const fields = tables[table]?.fields;
const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
indexedFields.set(modelName, []);
for (const field in fields) {
const attr = fields[field];
if (attr.index && !attr.unique) {
const fieldName = attr.fieldName || field;
indexedFields.get(modelName).push(fieldName);
}
}
}
const schema = produceSchema(schemaPrisma, (builder) => {
for (const table in tables) {
if (tables[table]?.disableMigrations) continue;
const originalTableName = table;
const customModelName = tables[table]?.modelName || table;
const modelName = capitalizeFirstLetter(getModelName(customModelName));
const fields = tables[table]?.fields;
function getType({ isBigint, isOptional, type }) {
if (type === "string") return isOptional ? "String?" : "String";
if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
if (type === "number") return isOptional ? "Int?" : "Int";
if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
if (type === "date") return isOptional ? "DateTime?" : "DateTime";
if (type === "json") {
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
return isOptional ? "Json?" : "Json";
}
if (type === "string[]") {
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
return "String[]";
}
if (type === "number[]") {
if (provider === "sqlite" || provider === "mysql") return "String";
return "Int[]";
}
}
function getFieldTypeParts(type) {
const isArray = type.endsWith("[]");
const typeWithoutArray = isArray ? type.slice(0, -2) : type;
const isOptional = typeWithoutArray.endsWith("?");
return {
fieldType: isOptional ? typeWithoutArray.slice(0, -1) : typeWithoutArray,
isArray,
isOptional
};
}
const prismaModel = builder.findByType("model", { name: modelName });
if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
else {
const useNumberId = options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
else if (useUUIDs && provider === "postgresql") builder.model(modelName).field("id", "String").attribute("id").attribute("default(dbgenerated(\"pg_catalog.gen_random_uuid()\"))").attribute("db.Uuid");
else builder.model(modelName).field("id", "String").attribute("id");
}
for (const field in fields) {
const attr = fields[field];
const fieldName = attr.fieldName || field;
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const useNumberId = options.advanced?.database?.generateId === "serial";
const fieldType = field === "id" && useNumberId ? getType({
isBigint: false,
isOptional: false,
type: "number"
}) : getType({
isBigint: attr?.bigint || false,
isOptional: attr?.required === false,
type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type
});
if (prismaModel) {
const isAlreadyExist = builder.findByType("field", {
name: fieldName,
within: prismaModel.properties
});
if (isAlreadyExist) {
if (fieldType && typeof isAlreadyExist.fieldType === "string") {
const fieldTypeParts = getFieldTypeParts(fieldType);
const existingFieldTypeParts = getFieldTypeParts(isAlreadyExist.fieldType);
if ((existingFieldTypeParts.fieldType === "Int" || existingFieldTypeParts.fieldType === "BigInt") && (fieldTypeParts.fieldType === "Int" || fieldTypeParts.fieldType === "BigInt")) {
isAlreadyExist.fieldType = fieldTypeParts.fieldType;
isAlreadyExist.optional = fieldTypeParts.isOptional || void 0;
isAlreadyExist.array = fieldTypeParts.isArray || void 0;
}
}
continue;
}
}
if (!fieldType) throw new Error(`Unsupported Prisma field type for model "${modelName}", field "${fieldName}"${attr.type ? ` (source type: "${attr.type}")` : ""}.`);
const fieldBuilder = builder.model(modelName).field(fieldName, fieldType);
if (field === "id") {
fieldBuilder.attribute("id");
if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
}
if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
if (attr.defaultValue !== void 0) {
if (Array.isArray(attr.defaultValue)) {
if (attr.type === "json") {
if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") {
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
continue;
}
const jsonArray = [];
for (const value of attr.defaultValue) jsonArray.push(value);
fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`);
continue;
}
if (attr.defaultValue.length === 0) {
fieldBuilder.attribute(`default([])`);
continue;
} else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") {
const valueArray = [];
for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value));
fieldBuilder.attribute(`default([${valueArray}])`);
} else if (typeof attr.defaultValue[0] === "number") {
const valueArray = [];
for (const value of attr.defaultValue) valueArray.push(`${value}`);
fieldBuilder.attribute(`default([${valueArray}])`);
}
} else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) {
if (Object.entries(attr.defaultValue).length === 0) {
fieldBuilder.attribute(`default("{}")`);
continue;
}
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
}
if (field === "createdAt") fieldBuilder.attribute("default(now())");
else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`);
else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`);
else if (typeof attr.defaultValue === "function") {}
}
if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
else if (attr.onUpdate) {}
if (attr.references) {
if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
const referencedOriginalModelName = getModelName(attr.references.model);
const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
let action = "Cascade";
if (attr.references.onDelete === "no action") action = "NoAction";
else if (attr.references.onDelete === "set null") action = "SetNull";
else if (attr.references.onDelete === "set default") action = "SetDefault";
else if (attr.references.onDelete === "restrict") action = "Restrict";
const relationField = `relation(fields: [${getFieldName({
model: originalTableName,
field: fieldName
})}], references: [${getFieldName({
model: attr.references.model,
field: attr.references.field
})}], onDelete: ${action})`;
builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${attr.required === false ? "?" : ""}`).attribute(relationField);
}
if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
}
if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {};
const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || [];
const isUnique = fkFieldAttr?.unique === true;
const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`;
if (!builder.findByType("field", {
name: fieldName,
within: prismaModel?.properties
})) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`);
}
const indexedFieldsForModel = indexedFields.get(modelName);
if (indexedFieldsForModel && indexedFieldsForModel.length > 0) for (const fieldName of indexedFieldsForModel) {
if (prismaModel) {
if (prismaModel.properties.some((v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args[0]?.value).includes(fieldName))) continue;
}
const field = Object.entries(fields).find(([key, attr]) => (attr.fieldName || key) === fieldName)?.[1];
let indexField = fieldName;
if (provider === "mysql" && field && field.type === "string") {
const useNumberId = options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (field.references?.field === "id" && (useNumberId || useUUIDs)) indexField = `${fieldName}`;
else indexField = `${fieldName}(length: 191)`;
}
builder.model(modelName).blockAttribute(`index([${indexField}])`);
}
const hasAttribute = builder.findByType("attribute", {
name: "map",
within: prismaModel?.properties
});
const hasChanged = customModelName !== originalTableName;
if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`);
}
});
const schemaChanged = schema.trim() !== schemaPrisma.trim();
return {
code: schemaChanged ? schema : "",
fileName: filePath,
overwrite: schemaPrismaExist && schemaChanged
};
};
const getNewPrisma = (provider, cwd) => {
const prismaVersion = getPrismaVersion(cwd);
const isV7 = prismaVersion && prismaVersion >= 7;
const clientProvider = isV7 ? "prisma-client" : "prisma-client-js";
if (isV7) return `generator client {
provider = "${clientProvider}"
}
datasource db {
provider = "${provider}"
}`;
return `generator client {
provider = "${clientProvider}"
}
datasource db {
provider = "${provider}"
url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
}`;
};
//#endregion
//#region src/generators/index.ts
const adapters = {
prisma: generatePrismaSchema,
drizzle: generateDrizzleSchema,
kysely: generateKyselySchema
};
const generateSchema = (opts) => {
const adapter = opts.adapter;
const generator = adapter.id in adapters ? adapters[adapter.id] : null;
if (generator) return generator(opts);
if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
code,
fileName,
overwrite
}));
throw new Error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`);
};
//#endregion
export { adapters, generateDrizzleSchema, generateKyselySchema, generatePrismaSchema, generateSchema };