import { execFile } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { promisify } from "node:util"; import type { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core"; import type { DBAdapter } from "@better-auth/core/db/adapter"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { prismaAdapter } from "better-auth/adapters/prisma"; import { organization, twoFactor, username } from "better-auth/plugins"; import Database from "better-sqlite3"; import { describe, expect, it } from "vitest"; import { generateSchema } from "../src/generators"; import { generateDrizzleSchema } from "../src/generators/drizzle"; import { generateKyselySchema } from "../src/generators/kysely"; import { generatePrismaSchema } from "../src/generators/prisma"; import { getPrismaVersion } from "../src/utils/get-package-info"; import { cliPath } from "./utils"; const execFileAsync = promisify(execFile); const compoundIndexPlugin = (): BetterAuthPlugin => ({ id: "compound-index-test", schema: { directoryUser: { modelName: "directory_user", fields: { connectionId: { type: "string", fieldName: "connection_id", }, externalId: { type: "string", fieldName: "external_id", }, status: { type: ["active", "suspended"], fieldName: "provisioning_status", }, }, indexes: [ { fields: ["connectionId", "externalId"], unique: true, }, { fields: ["connectionId", "status"] }, ], }, }, }); describe("generate", async () => { describe("command output paths", () => { it("should use adapter-specific filenames when output points to an existing directory", async () => { const cases = [ { adapter: "drizzle", dialect: "sqlite", expectedFileName: "auth-schema.ts", }, { adapter: "prisma", dialect: "sqlite", expectedFileName: "schema.prisma", }, { adapter: "kysely", expectedFileName: /^\d{4}-\d{2}-\d{2}T.*\.sql$/, }, ]; for (const testCase of cases) { const cacheDir = path.join( process.cwd(), "node_modules", ".cache", "generate-output-", ); fs.mkdirSync(path.dirname(cacheDir), { recursive: true }); const tmpDir = fs.mkdtempSync(cacheDir); const outputDir = path.join(tmpDir, "schema-output"); fs.mkdirSync(outputDir); fs.writeFileSync( path.join(tmpDir, "auth.ts"), `import { betterAuth } from "better-auth"; import Database from "better-sqlite3"; export const auth = betterAuth({ database: new Database(":memory:"), secret: "test-secret", baseURL: "http://localhost:3000", }); `, ); try { const args = [ cliPath, "generate", "--cwd", tmpDir, "--config", "auth.ts", "--adapter", testCase.adapter, "--output", "schema-output", "--yes", ]; if (testCase.dialect) { args.push("--dialect", testCase.dialect); } await execFileAsync(process.execPath, args, { cwd: tmpDir, env: { ...process.env, BETTER_AUTH_TELEMETRY_DISABLED: "true", }, }); const files = fs.readdirSync(outputDir); expect(files).toHaveLength(1); if (typeof testCase.expectedFileName === "string") { expect(files[0]).toBe(testCase.expectedFileName); } else { expect(files[0]).toMatch(testCase.expectedFileName); } } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } } }); /** * @see https://github.com/better-auth/better-auth/issues/10136 */ it("should remove a config import stub before generating a new Prisma schema", async () => { const cacheDir = path.join( process.cwd(), "node_modules", ".cache", "generate-output-", ); fs.mkdirSync(path.dirname(cacheDir), { recursive: true }); const tmpDir = fs.mkdtempSync(cacheDir); const outputPath = path.join(tmpDir, "schema.ts"); fs.writeFileSync( path.join(tmpDir, "auth.ts"), `import schema from "./schema"; import { betterAuth } from "better-auth"; export const auth = betterAuth({ secret: "test-secret", baseURL: "http://localhost:3000", }); export const __schema = schema; `, ); try { await execFileAsync( process.execPath, [ cliPath, "generate", "--cwd", tmpDir, "--config", "auth.ts", "--adapter", "prisma", "--dialect", "sqlite", "--output", "schema.ts", "--yes", ], { cwd: tmpDir, env: { ...process.env, BETTER_AUTH_TELEMETRY_DISABLED: "true", }, }, ); const schema = fs.readFileSync(outputPath, "utf-8"); expect(schema).toContain("generator client"); expect(schema).toContain("datasource db"); expect(schema).toContain("model User"); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } }); }); describe("prisma schema problems the generator cannot fix", () => { const existing = (issuer: string) => `generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Account { id String @id accountId String ${issuer} createdAt DateTime @default(now()) } `; const generate = async (schemaPrisma: string) => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ba-prisma-")); const file = path.join(tmpDir, "schema.prisma"); fs.writeFileSync(file, schemaPrisma); try { return await generatePrismaSchema({ file, adapter: prismaAdapter( {}, { provider: "postgresql" }, )({} as BetterAuthOptions), options: { database: prismaAdapter({}, { provider: "postgresql" }) }, }); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } }; it("reports a required column Better Auth never writes", async () => { const { schemaProblems } = await generate(existing("issuer String")); expect(schemaProblems).toHaveLength(1); expect(schemaProblems?.[0]).toContain( 'Column "issuer" on table "Account" is required but Better Auth never writes it', ); expect(schemaProblems?.[0]).toContain("Remove it from the Prisma schema"); }); it("accepts an optional or defaulted extra column", async () => { expect( (await generate(existing("issuer String?"))).schemaProblems, ).toEqual([]); expect( (await generate(existing('issuer String @default("")'))) .schemaProblems, ).toEqual([]); }); }); it("should generate prisma schema", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [twoFactor(), username()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema.prisma", ); }); it("should generate Prisma compound indexes with physical field names", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mysql", }, ), plugins: [compoundIndexPlugin()], }, }); expect(schema.code).toContain( '@@unique([connection_id, external_id], map: "directory_user_connection_id_external_id_uidx")', ); expect(schema.code).toContain( '@@index([connection_id, provisioning_status], map: "directory_user_connection_id_provisioning_status_idx")', ); expect(schema.code).toMatch(/connection_id\s+String\s+@db\.VarChar\(191\)/); expect(schema.code).toMatch(/external_id\s+String\s+@db\.VarChar\(191\)/); expect(schema.code).toMatch( /provisioning_status\s+String\s+@db\.VarChar\(191\)/, ); }); it("should bound existing MySQL string fields before adding compound indexes", async () => { const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), "prisma-compound-index-upgrade-"), ); const filePath = path.join(tmpDir, "schema.prisma"); const relativePath = path.relative(process.cwd(), filePath); fs.writeFileSync( filePath, ` generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model Directory_user { id String @id connection_id String @db.Text external_id String @db.Text provisioning_status String @db.Text @@map("directory_user") } `, ); try { const schema = await generatePrismaSchema({ file: relativePath, adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mysql", }, ), plugins: [compoundIndexPlugin()], }, }); expect(schema.code).toMatch( /connection_id\s+String\s+@db\.VarChar\(191\)/, ); expect(schema.code).toMatch(/external_id\s+String\s+@db\.VarChar\(191\)/); expect(schema.code).toMatch( /provisioning_status\s+String\s+@db\.VarChar\(191\)/, ); expect(schema.code).toContain( '@@unique([connection_id, external_id], map: "directory_user_connection_id_external_id_uidx")', ); } finally { fs.rmSync(tmpDir, { force: true, recursive: true }); } }); it("should reject a conflicting existing Prisma compound index", async () => { const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), "prisma-compound-index-"), ); const filePath = path.join(tmpDir, "schema.prisma"); const relativePath = path.relative(process.cwd(), filePath); fs.writeFileSync( filePath, ` generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model Directory_user { id String @id connection_id String external_id String provisioning_status String @@index([external_id], map: "Directory_User_Connection_Id_External_Id_UIDX") @@map("directory_user") } `, ); try { await expect( generatePrismaSchema({ file: relativePath, adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mysql", }, ), plugins: [compoundIndexPlugin()], }, }), ).rejects.toThrow( 'Prisma index "directory_user_connection_id_external_id_uidx" on model "Directory_user" does not match the configured fields and uniqueness.', ); } finally { fs.rmSync(tmpDir, { force: true, recursive: true }); } }); it("should name matching existing Prisma compound indexes", async () => { const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), "prisma-compound-index-name-"), ); const filePath = path.join(tmpDir, "schema.prisma"); const relativePath = path.relative(process.cwd(), filePath); fs.writeFileSync( filePath, ` generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model Directory_user { id String @id connection_id String external_id String provisioning_status String @@unique([connection_id, external_id]) @@index([connection_id, provisioning_status]) @@map("directory_user") } `, ); try { const schema = await generatePrismaSchema({ file: relativePath, adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mysql", }, ), plugins: [compoundIndexPlugin()], }, }); const schemaCode = schema.code ?? ""; expect( schemaCode.match(/@@unique\(\[connection_id, external_id\]/g), ).toHaveLength(1); expect( schemaCode.match(/@@index\(\[connection_id, provisioning_status\]/g), ).toHaveLength(1); expect(schemaCode).toContain( '@@unique([connection_id, external_id], map: "directory_user_connection_id_external_id_uidx")', ); expect(schemaCode).toContain( '@@index([connection_id, provisioning_status], map: "directory_user_connection_id_provisioning_status_idx")', ); } finally { fs.rmSync(tmpDir, { force: true, recursive: true }); } }); /** * @see https://github.com/better-auth/better-auth/issues/9717 */ const runBigintToggleTest = async ( fromBigint: boolean, toBigint: boolean, ) => { const fromRegex = fromBigint ? /aiCredits\s+BigInt/ : /aiCredits\s+Int(?!\w)/; const toRegex = toBigint ? /aiCredits\s+BigInt/ : /aiCredits\s+Int(?!\w)/; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-bigint-")); const relativePath = path.relative( process.cwd(), path.join(tmpDir, "schema.prisma"), ); try { const generate = (bigint: boolean) => generatePrismaSchema({ file: relativePath, adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), user: { additionalFields: { aiCredits: { type: "number", input: false, bigint, }, }, }, }, }); const first = await generate(fromBigint); expect(first.code).toBeDefined(); expect(first.code).toMatch(fromRegex); fs.writeFileSync(path.join(tmpDir, "schema.prisma"), first.code!); const updated = await generate(toBigint); expect(updated.overwrite).toBe(true); expect(updated.code).toMatch(toRegex); expect(updated.code).not.toMatch(fromRegex); } finally { fs.rmSync(tmpDir, { recursive: true }); } }; it("should update an existing prisma bigint number field to int", () => runBigintToggleTest(true, false)); it("should update an existing prisma int number field to bigint", () => runBigintToggleTest(false, true)); it("should not update existing prisma uuid id fields to serial ids", async () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-uuid-id-")); const schemaPath = path.join(tmpDir, "schema.prisma"); const relativePath = path.relative(process.cwd(), schemaPath); try { const generate = (generateId: "uuid" | "serial") => generatePrismaSchema({ file: relativePath, adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [twoFactor(), username()], advanced: { database: { generateId, }, }, }, }); const first = await generate("uuid"); expect(first.code).toBeDefined(); fs.writeFileSync(schemaPath, first.code!); const updated = await generate("serial"); const updatedSchema = updated.code || fs.readFileSync(schemaPath, "utf-8"); expect(updatedSchema).toMatch( /id\s+String\s+@id\s+@default\(dbgenerated\("pg_catalog\.gen_random_uuid\(\)"\)\)\s+@db\.Uuid/, ); expect(updatedSchema).toMatch(/userId\s+String\s+@db\.Uuid/); expect(updatedSchema).not.toMatch(/id\s+Int\s+@id.*@db\.Uuid/); expect(updatedSchema).not.toMatch(/userId\s+Int\s+@db\.Uuid/); } finally { fs.rmSync(tmpDir, { recursive: true }); } }); it("should generate prisma schema with number id", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [twoFactor(), username()], advanced: { database: { generateId: "serial", }, }, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-numberid.prisma", ); }); it("should generate prisma schema with uuid id", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [twoFactor(), username()], advanced: { database: { generateId: "uuid", }, }, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-uuid.prisma", ); }); it("should generate prisma schema for mongodb", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "mongodb", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mongodb", }, ), plugins: [twoFactor(), username()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-mongodb.prisma", ); }); it("should generate prisma schema for mysql", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mongodb", }, ), plugins: [twoFactor(), username()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-mysql.prisma", ); }); it("should generate prisma schema for mysql with custom model names", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "mysql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "mongodb", }, ), plugins: [ twoFactor(), username(), organization({ schema: { organization: { modelName: "workspace", }, invitation: { modelName: "workspaceInvitation", }, }, }), ], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-mysql-custom.prisma", ); }); it("should generate drizzle schema", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, }, ), plugins: [twoFactor(), username()], user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema.txt", ); }); it("should generate Drizzle compound indexes with physical field names", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, }, ), plugins: [compoundIndexPlugin()], }, }); expect(schema.code).toMatch( /uniqueIndex\("directory_user_connection_id_external_id_uidx"\)\.on\(\s*table\.connection_id,\s*table\.external_id,?\s*\)/, ); expect(schema.code).toMatch( /index\("directory_user_connection_id_provisioning_status_idx"\)\.on\(\s*table\.connection_id,\s*table\.provisioning_status,?\s*\)/, ); expect(schema.code).toMatch( /connection_id:\s*varchar\(["']connection_id["'], \{ length: 191 \}\)/, ); expect(schema.code).toMatch( /external_id:\s*varchar\(["']external_id["'], \{ length: 191 \}\)/, ); expect(schema.code).toMatch( /provisioning_status:\s*mysqlEnum\("provisioning_status", \[\s*"active",\s*"suspended",?\s*\]\)/, ); }); it("should reject duplicate Drizzle field-level and table-level indexes", async () => { await expect( generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { plugins: [ { id: "directory", schema: { directoryUser: { fields: { subject: { type: "string", index: true }, }, indexes: [{ fields: ["subject"] }], }, }, }, ], }, }), ).rejects.toThrow( 'Database index name "directoryUser_subject_idx" is already reserved by field-level index metadata on table "directoryUser".', ); }); it("should generate drizzle schema with number id", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, }, ), plugins: [twoFactor(), username()], advanced: { database: { generateId: "serial", }, }, user: { modelName: "custom_user", }, account: { modelName: "custom_account", }, session: { modelName: "custom_session", }, verification: { modelName: "custom_verification", }, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-number-id.txt", ); }); it("should treat fields with omitted required as notNull (default true)", async () => { const pluginWithOmittedRequired = (): BetterAuthPlugin => ({ id: "omitted-required-test", schema: { testTable: { fields: { requiredField: { type: "string", // required is omitted — should default to true }, explicitRequired: { type: "string", required: true, }, explicitOptional: { type: "string", required: false, }, }, }, }, }); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, plugins: [pluginWithOmittedRequired()], } as BetterAuthOptions, }); // Fields with omitted `required` should have .notNull() expect(schema.code).toContain( 'requiredField: text("required_field").notNull()', ); // Fields with explicit `required: true` should have .notNull() expect(schema.code).toContain( 'explicitRequired: text("explicit_required").notNull()', ); // Fields with explicit `required: false` should NOT have .notNull() expect(schema.code).not.toMatch(/explicitOptional:.*\.notNull\(\)/); }); it("should escape string default values so the generated schema stays valid TypeScript", async () => { const pluginWithQuotedDefault = (): BetterAuthPlugin => ({ id: "quoted-default-test", schema: { testTable: { fields: { greeting: { type: "string", defaultValue: 'say "hi"\\done', }, }, }, }, }); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, plugins: [pluginWithQuotedDefault()], } as BetterAuthOptions, }); // Quotes and backslashes must be escaped so the default is a valid // literal. The raw interpolation would emit `.default("say "hi"\done")` // and break the generated schema file. The generated schema is formatted // afterward, so the JSON-stringified value normalizes to single quotes. const escapedDefault = String.raw`.default('say "hi"\\done')`; expect(schema.code).toContain(escapedDefault); expect(schema.code).not.toContain(String.raw`.default("say "hi"\done")`); }); it("should not emit duplicate unique indexes for unique indexed fields", async () => { const pluginWithUniqueIndexedField = (): BetterAuthPlugin => ({ id: "unique-index-test", schema: { testTable: { fields: { slug: { type: "string", index: true, unique: true, }, }, }, }, }); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, plugins: [pluginWithUniqueIndexedField()], } as BetterAuthOptions, }); expect(schema.code).toContain('slug: text("slug").notNull().unique()'); expect(schema.code).not.toContain("slug_uidx"); }); it("should treat fields with omitted required as non-optional in prisma schema", async () => { const originalCwd = process.cwd(); const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), "prisma-required-test-"), ); try { fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify({ dependencies: { prisma: "^7.0.0" }, }), ); process.chdir(tmpDir); const pluginWithOmittedRequired = (): BetterAuthPlugin => ({ id: "omitted-required-test", schema: { testTable: { fields: { requiredField: { type: "string", // required is omitted — should default to true }, explicitRequired: { type: "string", required: true, }, explicitOptional: { type: "string", required: false, }, }, }, }, }); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql" }, )({} as BetterAuthOptions), options: { database: prismaAdapter({}, { provider: "postgresql" }), plugins: [pluginWithOmittedRequired()], }, }); // Fields with omitted `required` should NOT have "?" (= required) expect(schema.code).toMatch(/requiredField\s+String(?!\?)/); // Fields with explicit `required: true` should NOT have "?" expect(schema.code).toMatch(/explicitRequired\s+String(?!\?)/); // Fields with explicit `required: false` should have "?" expect(schema.code).toMatch(/explicitOptional\s+String\?/); } finally { process.chdir(originalCwd); fs.rmSync(tmpDir, { recursive: true }); } }); // Minimal plugin that reproduces the bug: two fields referencing the same model const testPlugin = (): BetterAuthPlugin => { return { id: "test", schema: { test: { fields: { userId: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, managerId: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, }, }, }, }; }; it("should generate drizzle schema without duplicate relations", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [testPlugin()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-duplicate-relations.txt", ); }); /** * @see https://github.com/better-auth/better-auth/issues/8849 */ it("should disambiguate duplicate relations when usePlural is enabled", async () => { const database = drizzleAdapter( {}, { provider: "sqlite", schema: {}, usePlural: true, }, ); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: database({} as BetterAuthOptions), options: { database, plugins: [testPlugin()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-drizzle-use-plural-duplicate-relations.txt", ); }); it("should emit one() for unique reverse relations", async () => { const uniqueProfilePlugin = (): BetterAuthPlugin => ({ id: "unique-profile", schema: { profile: { fields: { userId: { type: "string", required: true, unique: true, references: { model: "user", field: "id", onDelete: "cascade", }, }, }, }, }, }); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [uniqueProfilePlugin()], }, }); expect(schema.code).toContain("profile: one(profile)"); expect(schema.code).not.toMatch(/profile:\s*many\(profile\)/); }); it("should avoid colliding one-side relation keys after Id stripping", async () => { const collidingFkPlugin = ( fieldOrder: "owner-first" | "ownerId-first", ): BetterAuthPlugin => ({ id: "colliding-fk", schema: { project: { fields: fieldOrder === "owner-first" ? { owner: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, ownerId: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, } : { ownerId: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, owner: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, }, }, }, }); for (const fieldOrder of ["owner-first", "ownerId-first"] as const) { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [collidingFkPlugin(fieldOrder)], }, }); expect(schema.code).toBeTruthy(); expect(schema.code).toContain('relationName: "project_owner"'); expect(schema.code).toContain('relationName: "project_ownerId"'); const projectRelations = schema.code!.match( /export const projectRelations = relations\([\s\S]*?\n\}\)\);/, )?.[0]; expect(projectRelations).toBeTruthy(); const oneKeys = [ ...projectRelations!.matchAll(/^\s+(\w+):\s+one\(user,/gm), ].map((match) => match[1]); expect(oneKeys).toHaveLength(2); expect(new Set(oneKeys).size).toBe(2); if (fieldOrder === "owner-first") { expect(oneKeys).toEqual(expect.arrayContaining(["owner", "ownerId"])); } else { // ownerId is stripped to `owner` first, so the later `owner` // field needs a unique fallback key. expect(oneKeys).toEqual(expect.arrayContaining(["owner", "owner_2"])); } } }); // Plugin that tests multiple relations to different models (should be combined) const multiRelationPlugin = (): BetterAuthPlugin => { return { id: "multi-relation", schema: { project: { fields: { ownerId: { type: "string", required: false, references: { model: "user", field: "id", onDelete: "set null", }, }, sessionId: { type: "string", required: false, references: { model: "session", field: "id", onDelete: "set null", }, }, }, }, }, }; }; it("should combine multiple relations to different models into single export", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, }, ), plugins: [multiRelationPlugin()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-multi-relation.txt", ); }); /** * @see https://github.com/better-auth/better-auth/issues/10587 */ it("should generate drizzle schema with schemaName for PostgreSQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, schemaName: "auth", }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, schemaName: "auth", }, ), plugins: [twoFactor(), username()], }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-pg-with-schema-name.txt", ); // Should declare the schema expect(schema.code).toContain('export const authSchema = pgSchema("auth")'); // Should use schema.table() instead of pgTable() for table definitions expect(schema.code).toContain("authSchema.table"); // Should not use or import pgTable() when schemaName is set expect(schema.code).not.toMatch(/export const \w+ = pgTable\(/); expect(schema.code).not.toContain("pgTable"); }); it("should generate drizzle schema without schemaName for PostgreSQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, }, ), plugins: [twoFactor(), username()], }, }); // Should not import pgSchema when schemaName is undefined expect(schema.code).not.toContain( 'import { pgSchema } from "drizzle-orm/pg-core"', ); // Should use pgTable() for table definitions expect(schema.code).toMatch(/export const \w+ = pgTable\(/); expect(schema.code).not.toContain("pgSchema("); }); it("should ignore schemaName for SQLite", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "sqlite", schema: {}, schemaName: "auth", // Should be ignored }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "sqlite", schema: {}, schemaName: "auth", }, ), plugins: [twoFactor(), username()], }, }); // Should not import pgSchema (SQLite doesn't support schemas) expect(schema.code).not.toContain("pgSchema"); // Should use sqliteTable() for table definitions expect(schema.code).toMatch(/export const \w+ = sqliteTable\(/); expect(schema.code).not.toContain("pgSchema("); }); it("should ignore schemaName for MySQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "mysql", schema: {}, schemaName: "auth", // Should be ignored }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "mysql", schema: {}, schemaName: "auth", }, ), plugins: [twoFactor(), username()], }, }); // Should not import pgSchema (MySQL doesn't support schemas in the same way) expect(schema.code).not.toContain("pgSchema"); // Should use mysqlTable() for table definitions expect(schema.code).toMatch(/export const \w+ = mysqlTable\(/); expect(schema.code).not.toContain("pgSchema("); }); it("should generate drizzle schema with schemaName containing hyphens", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: drizzleAdapter( {}, { provider: "pg", schema: {}, schemaName: "my-auth-schema", }, )({} as BetterAuthOptions), options: { database: drizzleAdapter( {}, { provider: "pg", schema: {}, schemaName: "my-auth-schema", }, ), plugins: [], }, }); // Should convert hyphenated schema name to valid identifier expect(schema.code).toContain( 'export const myauthschemaSchema = pgSchema("my-auth-schema")', ); expect(schema.code).toContain("myauthschemaSchema.table"); }); it("should generate kysely schema", async () => { const schema = await generateKyselySchema({ file: "test.sql", options: { database: new Database(":memory:"), }, adapter: {} as any, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/migrations.sql", ); }); it("should throw for unsupported additionalFields type in migrations", async () => { await expect( generateKyselySchema({ file: "test.sql", options: { database: new Database(":memory:"), user: { additionalFields: { is_subscribed: { type: "object" } as unknown as any, } as any, }, }, adapter: {} as any, }), ).rejects.toThrow(/Unsupported field type/); }); }); describe("JSON field support in CLI generators", () => { it("should generate Drizzle schema with JSON fields for PostgreSQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { preferences: { type: "json", }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain("preferences: jsonb("); }); it("should generate Drizzle schema with JSON fields for MySQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "mysql", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { preferences: { type: "json", }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain("preferences: json("); }); it("should generate Drizzle schema with JSON fields for SQLite", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "sqlite", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { preferences: { type: "json", }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain("preferences: text("); }); it("should generate Prisma schema with JSON fields", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: { id: "prisma", options: {}, } as any, options: { database: {} as any, user: { additionalFields: { preferences: { type: "json", }, }, }, } as BetterAuthOptions, }); // required omitted → defaults to true → non-nullable expect(schema.code).toMatch(/preferences\s+Json(?!\?)/); }); it("should generate Prisma schema with JSON default values of arrays and objects", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: { id: "prisma", options: {}, } as any, options: { database: {} as any, user: { additionalFields: { preferences: { type: "json", defaultValue: { premiumuser: true, }, }, metadata: { type: "json", defaultValue: [ { name: "john", subscribed: false, }, { name: "doe", subscribed: true }, ], }, }, }, } as BetterAuthOptions, }); // required omitted → defaults to true → non-nullable expect(schema.code).toMatch(/preferences\s+Json(?!\?)/); expect(schema.code).toContain('@default("{\\"premiumuser\\":true}")'); expect(schema.code).toContain( '@default("[{\\"name\\":\\"john\\",\\"subscribed\\":false},{\\"name\\":\\"doe\\",\\"subscribed\\":true}]")', ); }); }); describe("Enum field support in Drizzle schemas", () => { it("should generate Drizzle schema with enum fields for PostgreSQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { role: { type: ["admin", "user", "guest"], required: true, }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain( 'role: text("role", { enum: ["admin", "user", "guest"] })', ); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-pg-enum.txt", ); }); it("should generate Drizzle schema with enum fields for MySQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "mysql", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { status: { type: ["active", "inactive", "pending"], required: false, }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain("mysqlEnum"); expect(schema.code).toContain( 'status: mysqlEnum("status", ["active", "inactive", "pending"])', ); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-mysql-enum.txt", ); }); it("should generate Drizzle schema with enum fields for SQLite", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "sqlite", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { priority: { type: ["high", "medium", "low"], }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain('text("priority", { enum: ['); expect(schema.code).toContain( 'priority: text("priority", { enum: ["high", "medium", "low"] })', ); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-sqlite-enum.txt", ); }); it("should include correct imports for enum fields in MySQL", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "mysql", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { status: { type: ["active", "inactive"], }, }, }, } as BetterAuthOptions, }); expect(schema.code).toMatch( /import.*mysqlEnum.*from.*drizzle-orm\/mysql-core/s, ); }); it("should not include enum imports when no enum fields are present", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, user: { additionalFields: { name: { type: "string", }, }, }, } as BetterAuthOptions, }); expect(schema.code).not.toContain("enum"); }); it("should throw for unsupported additionalFields type in migrations", async () => { await expect( generateKyselySchema({ file: "test.sql", options: { database: new Database(":memory:"), user: { additionalFields: { is_subscribed: { type: "object" } as unknown as any, } as any, }, }, adapter: {} as any, }), ).rejects.toThrow(/Unsupported field type/); }); }); describe("Drizzle array defaultValue serialization", () => { /** * @see https://github.com/better-auth/better-auth/issues/10046 */ it("emits a JS array literal for string[] additionalField defaultValue", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {} }, } as any, options: { database: {} as any, user: { additionalFields: { roles: { type: "string[]", required: true, defaultValue: ["customer"], input: false, }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain( 'roles: text("roles").array().default(["customer"]).notNull()', ); expect(schema.code).not.toContain(".default(customer)"); }); it("emits a JS array literal for number[] additionalField defaultValue", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {} }, } as any, options: { database: {} as any, user: { additionalFields: { scores: { type: "number[]", required: true, defaultValue: [1, 2, 3], }, }, }, } as BetterAuthOptions, }); expect(schema.code).toContain( 'scores: integer("scores").array().default([1, 2, 3]).notNull()', ); }); }); describe("usePlural schema generation", () => { it("should generate drizzle schema with usePlural option", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, usePlural: true, adapterConfig: { usePlural: true }, }, } as any, options: { database: {} as any, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/auth-schema-drizzle-use-plural.txt", ); }); it("should generate prisma schema with usePlural option", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: { id: "prisma", options: { provider: "postgresql", usePlural: true, adapterConfig: { usePlural: true }, }, } as any, options: { database: {} as any, }, }); await expect(schema.code).toMatchFileSnapshot( "./__snapshots__/schema-prisma-use-plural.prisma", ); }); }); describe("Prisma v7 compatibility", () => { it("should detect Prisma version from package.json", () => { // Test with Prisma v7 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-v7-test-")); const packageJson = { dependencies: { prisma: "^7.0.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); const version = getPrismaVersion(tmpDir); expect(version).toBe(7); fs.rmSync(tmpDir, { recursive: true }); }); it("should detect Prisma v5 from package.json", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-v5-test-")); const packageJson = { dependencies: { prisma: "^5.0.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); const version = getPrismaVersion(tmpDir); expect(version).toBe(5); fs.rmSync(tmpDir, { recursive: true }); }); it("should detect Prisma version from @prisma/client", () => { const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), "prisma-client-test-"), ); const packageJson = { devDependencies: { "@prisma/client": "~7.1.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); const version = getPrismaVersion(tmpDir); expect(version).toBe(7); fs.rmSync(tmpDir, { recursive: true }); }); it("should return null when Prisma is not installed", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "no-prisma-test-")); const packageJson = { dependencies: {}, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); const version = getPrismaVersion(tmpDir); expect(version).toBeNull(); fs.rmSync(tmpDir, { recursive: true }); }); it("should generate schema with prisma-client provider for v7+", async () => { const originalCwd = process.cwd(); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-v7-schema-")); try { const packageJson = { dependencies: { prisma: "^7.0.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); process.chdir(tmpDir); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [], }, }); expect(schema.code).toContain('provider = "prisma-client"'); expect(schema.code).not.toContain('provider = "prisma-client-js"'); // Prisma v7+ should not include url in datasource (configured in prisma.config.ts) expect(schema.code).not.toContain("url"); } finally { process.chdir(originalCwd); fs.rmSync(tmpDir, { recursive: true }); } }); it("should generate schema with prisma-client-js provider for v5", async () => { const originalCwd = process.cwd(); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-v5-schema-")); try { // Create package.json with Prisma v5 const packageJson = { dependencies: { prisma: "^5.0.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); // Change to temp directory process.chdir(tmpDir); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [], }, }); // Check that the schema uses prisma-client-js for v5 expect(schema.code).toContain('provider = "prisma-client-js"'); expect(schema.code).not.toContain('provider = "prisma-client"'); } finally { // Restore original directory process.chdir(originalCwd); fs.rmSync(tmpDir, { recursive: true }); } }); it("should generate schema with prisma-client-js provider for v6", async () => { const originalCwd = process.cwd(); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "prisma-v6-schema-")); try { // Create package.json with Prisma v6 const packageJson = { dependencies: { prisma: "^6.0.0", }, }; fs.writeFileSync( path.join(tmpDir, "package.json"), JSON.stringify(packageJson), ); // Change to temp directory process.chdir(tmpDir); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql", }, )({} as BetterAuthOptions), options: { database: prismaAdapter( {}, { provider: "postgresql", }, ), plugins: [], }, }); // Check that the schema uses prisma-client-js for v6 expect(schema.code).toContain('provider = "prisma-client-js"'); expect(schema.code).not.toContain('provider = "prisma-client"'); } finally { // Restore original directory process.chdir(originalCwd); fs.rmSync(tmpDir, { recursive: true }); } }); }); describe("--adapter flag support (mock adapter)", () => { // Helper function to create a mock adapter similar to createMockAdapter in generate.ts function createMockAdapter(adapterId: string, provider?: string): DBAdapter { return { id: adapterId, create: async () => { throw new Error("Mock adapter methods should not be called"); }, findOne: async () => { throw new Error("Mock adapter methods should not be called"); }, findMany: async () => { throw new Error("Mock adapter methods should not be called"); }, count: async () => { throw new Error("Mock adapter methods should not be called"); }, update: async () => { throw new Error("Mock adapter methods should not be called"); }, updateMany: async () => { throw new Error("Mock adapter methods should not be called"); }, delete: async () => { throw new Error("Mock adapter methods should not be called"); }, deleteMany: async () => { throw new Error("Mock adapter methods should not be called"); }, consumeOne: async () => { throw new Error("Mock adapter methods should not be called"); }, incrementOne: async () => { throw new Error("Mock adapter methods should not be called"); }, transaction: async (callback) => { throw new Error("Mock adapter methods should not be called"); }, options: { adapterConfig: { adapterId, }, ...(provider && { provider }), }, }; } it("should generate prisma schema with mock adapter", async () => { const mockAdapter = createMockAdapter("prisma", "postgresql"); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [twoFactor(), username()], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("model User"); expect(schema.code).toContain("model Account"); expect(schema.code).toContain("model Session"); }); it("should generate drizzle schema with mock adapter and provider", async () => { const mockAdapter = createMockAdapter("drizzle", "pg"); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [twoFactor(), username()], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("export const user"); expect(schema.code).toContain("export const account"); expect(schema.code).toContain("export const session"); }); it("should throw error when generating drizzle schema without provider", async () => { const mockAdapter = createMockAdapter("drizzle"); await expect( generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }), ).rejects.toThrow(/Database provider type is undefined/); }); it("should generate kysely schema with mock adapter", async () => { const mockAdapter = createMockAdapter("kysely"); const schema = await generateKyselySchema({ file: "test.sql", adapter: mockAdapter, options: { database: new Database(":memory:"), }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("create table"); }); it("should route to correct generator using generateSchema with mock prisma adapter", async () => { const mockAdapter = createMockAdapter("prisma", "postgresql"); const schema = await generateSchema({ adapter: mockAdapter, file: "test.prisma", options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("model User"); expect(schema.fileName).toBe("test.prisma"); }); it("should route to correct generator using generateSchema with mock drizzle adapter", async () => { const mockAdapter = createMockAdapter("drizzle", "pg"); const schema = await generateSchema({ adapter: mockAdapter, file: "test.drizzle", options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("export const user"); expect(schema.fileName).toBe("test.drizzle"); }); it("should route to correct generator using generateSchema with mock kysely adapter", async () => { const mockAdapter = createMockAdapter("kysely"); const schema = await generateSchema({ adapter: mockAdapter, file: "test.sql", options: { database: new Database(":memory:"), }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("create table"); expect(schema.fileName).toBe("test.sql"); }); it("should throw error for unsupported adapter id", async () => { const mockAdapter = createMockAdapter("unsupported-adapter"); let error: Error | undefined; try { await generateSchema({ adapter: mockAdapter, file: "test.txt", options: { database: {} as any, plugins: [], }, }); } catch (e) { error = e as Error; } expect(error).toBeDefined(); }); it("should generate prisma schema with mock adapter and usePlural option", async () => { const mockAdapter: DBAdapter = { ...createMockAdapter("prisma", "postgresql"), options: { adapterConfig: { adapterId: "prisma", usePlural: true, }, provider: "postgresql", }, }; const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("model Users"); expect(schema.code).toContain("model Accounts"); }); it("should generate drizzle schema with mock adapter and usePlural option", async () => { const mockAdapter: DBAdapter = { ...createMockAdapter("drizzle", "pg"), options: { adapterConfig: { adapterId: "drizzle", usePlural: true, }, provider: "pg", }, }; const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("export const users"); expect(schema.code).toContain("export const accounts"); }); }); describe("--dialect flag support", () => { // Helper function that matches the implementation in generate.ts function createMockAdapterWithDialect( adapterId: string, dialect?: string, ): DBAdapter { let provider: string | undefined; if (dialect) { if (adapterId === "drizzle") { if (dialect === "postgresql") { provider = "pg"; } else if (dialect === "mysql" || dialect === "sqlite") { provider = dialect; } else { provider = dialect === "pg" ? "pg" : undefined; } } else if (adapterId === "prisma") { provider = dialect; } } return { id: adapterId, create: async () => { throw new Error("Mock adapter methods should not be called"); }, findOne: async () => { throw new Error("Mock adapter methods should not be called"); }, findMany: async () => { throw new Error("Mock adapter methods should not be called"); }, count: async () => { throw new Error("Mock adapter methods should not be called"); }, update: async () => { throw new Error("Mock adapter methods should not be called"); }, updateMany: async () => { throw new Error("Mock adapter methods should not be called"); }, delete: async () => { throw new Error("Mock adapter methods should not be called"); }, deleteMany: async () => { throw new Error("Mock adapter methods should not be called"); }, consumeOne: async () => { throw new Error("Mock adapter methods should not be called"); }, incrementOne: async () => { throw new Error("Mock adapter methods should not be called"); }, transaction: async (callback) => { throw new Error("Mock adapter methods should not be called"); }, options: { adapterConfig: { adapterId, }, ...(provider && { provider }), }, }; } it("should map postgresql dialect to pg provider for drizzle", async () => { const mockAdapter = createMockAdapterWithDialect("drizzle", "postgresql"); expect(mockAdapter.options?.provider).toBe("pg"); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("pg"); expect(schema.code).toContain("export const user"); }); it("should map mysql dialect to mysql provider for drizzle", async () => { const mockAdapter = createMockAdapterWithDialect("drizzle", "mysql"); expect(mockAdapter.options?.provider).toBe("mysql"); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("mysql"); expect(schema.code).toContain("export const user"); }); it("should map sqlite dialect to sqlite provider for drizzle", async () => { const mockAdapter = createMockAdapterWithDialect("drizzle", "sqlite"); expect(mockAdapter.options?.provider).toBe("sqlite"); const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("sqlite"); expect(schema.code).toContain("export const user"); }); it("should use postgresql dialect directly for prisma", async () => { const mockAdapter = createMockAdapterWithDialect("prisma", "postgresql"); expect(mockAdapter.options?.provider).toBe("postgresql"); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain('provider = "postgresql"'); expect(schema.code).toContain("model User"); }); it("should use mysql dialect directly for prisma", async () => { const mockAdapter = createMockAdapterWithDialect("prisma", "mysql"); expect(mockAdapter.options?.provider).toBe("mysql"); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain('provider = "mysql"'); expect(schema.code).toContain("model User"); }); it("should use sqlite dialect directly for prisma", async () => { const mockAdapter = createMockAdapterWithDialect("prisma", "sqlite"); expect(mockAdapter.options?.provider).toBe("sqlite"); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain('provider = "sqlite"'); expect(schema.code).toContain("model User"); }); it("should use mongodb dialect directly for prisma", async () => { const mockAdapter = createMockAdapterWithDialect("prisma", "mongodb"); expect(mockAdapter.options?.provider).toBe("mongodb"); const schema = await generatePrismaSchema({ file: "test.prisma", adapter: mockAdapter, options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain('provider = "mongodb"'); expect(schema.code).toContain("model User"); }); it("should work with generateSchema routing for drizzle with dialect", async () => { const mockAdapter = createMockAdapterWithDialect("drizzle", "postgresql"); const schema = await generateSchema({ adapter: mockAdapter, file: "test.drizzle", options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain("pg"); expect(schema.fileName).toBe("test.drizzle"); }); it("should work with generateSchema routing for prisma with dialect", async () => { const mockAdapter = createMockAdapterWithDialect("prisma", "mysql"); const schema = await generateSchema({ adapter: mockAdapter, file: "test.prisma", options: { database: {} as any, plugins: [], }, }); expect(schema.code).toBeDefined(); expect(schema.code).toContain('provider = "mysql"'); expect(schema.fileName).toBe("test.prisma"); }); const pluginWithDisabledMigration = (): BetterAuthPlugin => ({ id: "disabled-migration-test", schema: { emittedTable: { fields: { name: { type: "string", required: true }, skippedTableId: { type: "string", required: false, references: { model: "skippedTable", field: "id", }, }, }, }, skippedTable: { fields: { name: { type: "string", required: true }, }, disableMigration: true, }, }, }); it("should not emit drizzle tables with disableMigration", async () => { const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", schema: {}, }, } as any, options: { database: {} as any, plugins: [pluginWithDisabledMigration()], } as BetterAuthOptions, }); expect(schema.code).toContain("emittedTable"); expect(schema.code).toContain("skippedTableId"); expect(schema.code).not.toContain("export const skippedTable"); expect(schema.code).not.toContain("references(() => skippedTable"); expect(schema.code).not.toContain("skippedTable: one(skippedTable"); }); it("should not emit drizzle relations when a rendered model name references disabled migrations", async () => { const plugin: BetterAuthPlugin = { id: "disabled-rendered-reference-test", schema: { emittedTable: { fields: { skippedTableId: { type: "string", required: false, references: { model: "skippedTables", field: "id", }, }, }, }, skippedTable: { fields: { name: { type: "string", required: true }, }, disableMigration: true, }, }, }; const schema = await generateDrizzleSchema({ file: "test.drizzle", adapter: { id: "drizzle", options: { provider: "pg", adapterConfig: { usePlural: true }, schema: {}, }, } as any, options: { database: {} as any, plugins: [plugin], } as BetterAuthOptions, }); expect(schema.code).toContain("skippedTableId"); expect(schema.code).not.toContain("references(() => skippedTables"); expect(schema.code).not.toContain("skippedTables: one(skippedTables"); }); it("should not emit prisma models with disableMigration", async () => { const schema = await generatePrismaSchema({ file: "test.prisma", adapter: prismaAdapter( {}, { provider: "postgresql" }, )({} as BetterAuthOptions), options: { database: prismaAdapter({}, { provider: "postgresql" }), plugins: [pluginWithDisabledMigration()], }, }); expect(schema.code).toContain("EmittedTable"); expect(schema.code).toContain("skippedTableId"); expect(schema.code).not.toContain("SkippedTable"); expect(schema.code).not.toContain("skippedtable"); }); });