fix: treat generateId "serial" as numeric ID and correct UUID column types across adapters (#5823)

This commit is contained in:
Maxwell
2025-11-12 04:04:24 +10:00
committed by GitHub
parent a7682accaa
commit 705f7434c7
38 changed files with 943 additions and 101 deletions

View File

@@ -22,6 +22,7 @@ exports[`init > should match config 1`] = `
"supportsDates": false,
"supportsJSON": false,
"supportsNumericIds": true,
"supportsUUIDs": false,
"transaction": false,
"usePlural": undefined,
},

View File

@@ -49,6 +49,7 @@ export const createAdapterFactory =
supportsJSON: cfg.supportsJSON ?? false,
adapterName: cfg.adapterName ?? cfg.adapterId,
supportsNumericIds: cfg.supportsNumericIds ?? true,
supportsUUIDs: cfg.supportsUUIDs ?? false,
transaction: cfg.transaction ?? false,
disableTransformInput: cfg.disableTransformInput ?? false,
disableTransformOutput: cfg.disableTransformOutput ?? false,
@@ -257,8 +258,22 @@ export const createAdapterFactory =
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
const shouldGenerateId =
!config.disableIdGeneration && !useNumberId && !forceAllowId;
const useUUIDs = options.advanced?.database?.generateId === "uuid";
let shouldGenerateId: boolean = (() => {
if (config.disableIdGeneration) {
return false;
} else if (useNumberId && !forceAllowId) {
// if force allow is true, then we should be using their custom provided id.
return false;
} else if (useUUIDs && !config.supportsUUIDs) {
// should only generate UUIDs via JS if the database doesn't support natively generating UUIDs.
return true;
} else {
return true;
}
})();
const model = getDefaultModelName(customModelName ?? "id");
return {
type: useNumberId ? "number" : "string",
@@ -280,16 +295,75 @@ export const createAdapterFactory =
model,
});
}
if (generateId === "uuid") {
return crypto.randomUUID();
}
if (config.customIdGenerator) {
return config.customIdGenerator({ model });
}
if (generateId === "uuid") {
return crypto.randomUUID();
}
return defaultGenerateId();
},
}
: {}),
transform: {
input: (value) => {
// Uncomment if need to debug id transformation
// console.log(`transforming id: `, {
// id: value,
// ...(useNumberId ? { useNumberId } : {}),
// ...(useUUIDs ? { useUUIDs } : {}),
// ...(forceAllowId ? { forceAllowId } : {}),
// });
if (!value) return undefined;
if (useNumberId) {
const numberValue = Number(value);
// if invalid number, fallback to DB generated number id.
if (isNaN(numberValue)) {
return undefined;
}
return numberValue;
}
if (useUUIDs) {
// if it's generated by us, then we should return the value as is.
if (shouldGenerateId && !forceAllowId) return value;
if (config.disableIdGeneration) return undefined;
// if DB will handle UUID generation, then we should return undefined.
if (config.supportsUUIDs) return undefined;
// if forceAllowId is true, it means we should be using the ID provided during the adapter call.
if (forceAllowId && typeof value === "string") {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (uuidRegex.test(value)) {
return value;
} else {
const err = new Error();
const stack = err.stack
?.split("\n")
.filter((_, i) => i !== 1)
.join("\n")
.replace("Error:", "");
logger.warn(
"[Adapter Factory] - Invalid UUID value for field `id` provided when `forceAllowId` is true. Generating a new UUID.",
stack,
);
}
}
// if the value is not a string, and the database doesn't support generating it's own UUIDs, then we should be generating the UUID.
if (typeof value !== "string" && !config.supportsUUIDs) {
return crypto.randomUUID();
}
return undefined;
}
return value;
},
output: (value) => {
if (!value) return undefined;
return String(value);
},
},
} satisfies DBFieldAttribute;
};
@@ -325,15 +399,13 @@ export const createAdapterFactory =
const fields = schema[defaultModelName]!.fields;
const newMappedKeys = config.mapKeysTransformInput ?? {};
if (
!config.disableIdGeneration &&
!options.advanced?.database?.useNumberId
) {
fields.id = idField({
customModelName: defaultModelName,
forceAllowId: forceAllowId && "id" in data,
});
}
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
fields.id = idField({
customModelName: defaultModelName,
forceAllowId: forceAllowId && "id" in data,
});
for (const field in fields) {
let value = data[field];
const fieldAttributes = fields[field];
@@ -378,10 +450,7 @@ export const createAdapterFactory =
newValue = await fieldAttributes!.transform.input(newValue);
}
if (
fieldAttributes!.references?.field === "id" &&
options.advanced?.database?.useNumberId
) {
if (fieldAttributes!.references?.field === "id" && useNumberId) {
if (Array.isArray(newValue)) {
newValue = newValue.map((x) => (x !== null ? Number(x) : null));
} else {
@@ -412,7 +481,7 @@ export const createAdapterFactory =
action,
field: newFieldName,
fieldAttributes: fieldAttributes!,
model: defaultModelName,
model: getModelName(defaultModelName),
schema,
options,
});
@@ -495,7 +564,7 @@ export const createAdapterFactory =
field: newFieldName,
fieldAttributes: field,
select,
model: unsafe_model,
model: getModelName(unsafe_model),
schema,
options,
});
@@ -549,11 +618,15 @@ export const createAdapterFactory =
model: defaultModelName,
});
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
if (
defaultFieldName === "id" ||
fieldAttr!.references?.field === "id"
) {
if (options.advanced?.database?.useNumberId) {
if (useNumberId) {
if (Array.isArray(value)) {
newValue = value.map(Number);
} else {
@@ -649,7 +722,16 @@ export const createAdapterFactory =
let thisTransactionId = transactionId;
const model = getModelName(unsafeModel);
unsafeModel = getDefaultModelName(unsafeModel);
if ("id" in unsafeData && !forceAllowId) {
if (
"id" in unsafeData &&
typeof unsafeData.id !== "undefined" &&
!forceAllowId
) {
// The reason why `forceAllowId` was introduced was because we used to handle
// id generation ourselves (eg adapter.create({ data: { id: "123" } }))
// This was bad as certain things (such as number ids) would not work as expected.
// Since then, we have introduced the `forceAllowId` parameter to allow users to
// bypass this check. Otherwise, we would throw a warning stating that the id will be ignored
logger.warn(
`[${config.adapterName}] - You are trying to create a record with an id. This is not allowed as we handle id generation for you, unless you pass in the \`forceAllowId\` parameter. The id will be ignored.`,
);

View File

@@ -19,9 +19,6 @@ export type AdapterFactoryOptions = {
adapter: AdapterFactoryCustomizeAdapterCreator;
};
/**
* @deprecated Use `DBAdapterFactoryConfig` from `@better-auth/core/db/adapter` instead.
*/
export interface AdapterFactoryConfig
extends Omit<DBAdapterFactoryConfig<BetterAuthOptions>, "transaction"> {
/**

View File

@@ -119,6 +119,7 @@ export const createTestSuite = <
*/
alwaysMigrate?: boolean | undefined;
prefixTests?: string | undefined;
customIdGenerator?: () => any | Promise<any> | undefined;
},
tests: (
helpers: {
@@ -305,8 +306,18 @@ export const createTestSuite = <
return newData;
};
const idGenerator = async () => {
if (config.customIdGenerator) {
return config.customIdGenerator();
}
if (helpers.customIdGenerator) {
return helpers.customIdGenerator();
}
return generateId();
};
const generateModel: GenerateFn = async (model: string) => {
const id = (await helpers.customIdGenerator?.()) || generateId();
const id = await idGenerator();
const randomDate = new Date(
Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 365,
);

View File

@@ -453,6 +453,7 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
adapterName: "Drizzle Adapter",
usePlural: config.usePlural ?? false,
debugLogs: config.debugLogs ?? false,
supportsUUIDs: config.provider === "pg" ? true : false,
transaction:
(config.transaction ?? false)
? (cb) =>

View File

@@ -8,6 +8,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { drizzleAdapter } from "../drizzle-adapter";
import { generateDrizzleSchema, resetGenerationCount } from "./generate-schema";
@@ -65,6 +66,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite(),
],
async onFinish() {
await mysqlDB.end();

View File

@@ -7,6 +7,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { drizzleAdapter } from "../drizzle-adapter";
import { generateDrizzleSchema, resetGenerationCount } from "./generate-schema";
@@ -61,6 +62,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite(),
],
async onFinish() {
await cleanupDatabase(true);

View File

@@ -9,6 +9,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { drizzleAdapter } from "../drizzle-adapter";
import {
@@ -65,6 +66,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite(),
],
async onFinish() {
clearSchemaCache();

View File

@@ -335,6 +335,7 @@ export const kyselyAdapter = (
? false
: true,
supportsJSON: false,
supportsUUIDs: config?.type === "postgres" ? true : false,
transaction: config?.transaction
? (cb) =>
db.transaction().execute((trx) => {

View File

@@ -9,6 +9,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { kyselyAdapter } from "../kysely-adapter";
@@ -307,6 +308,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite({ showDB }),
numberIdTestSuite(),
uuidTestSuite(),
],
async onFinish() {
kyselyDB.destroy();

View File

@@ -8,6 +8,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { kyselyAdapter } from "../kysely-adapter";
@@ -47,6 +48,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite(),
],
async onFinish() {
await mysqlDB.end();

View File

@@ -8,6 +8,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { kyselyAdapter } from "../kysely-adapter";
@@ -35,7 +36,8 @@ const { execute } = await testAdapter({
const opts = Object.assign(betterAuthOptions, {
database: pgDB,
} satisfies BetterAuthOptions);
const { runMigrations } = await getMigrations(opts);
const { runMigrations, compileMigrations } = await getMigrations(opts);
// console.log(await compileMigrations());
await runMigrations();
},
tests: [
@@ -43,6 +45,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite({}),
],
async onFinish() {
await pgDB.end();

View File

@@ -9,6 +9,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { kyselyAdapter } from "../kysely-adapter";
@@ -45,6 +46,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite({}),
],
async onFinish() {
database.close();

View File

@@ -4,8 +4,8 @@ import {
authFlowTestSuite,
normalTestSuite,
numberIdTestSuite,
performanceTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../tests";
import { memoryAdapter } from "./memory-adapter";
@@ -29,7 +29,7 @@ const { execute } = await testAdapter({
transactionsTestSuite({ disableTests: { ALL: true } }),
authFlowTestSuite(),
numberIdTestSuite(),
performanceTestSuite(),
uuidTestSuite(),
],
async onFinish() {},
});

View File

@@ -26,11 +26,10 @@ export const memoryAdapter = (
usePlural: false,
debugLogs: config?.debugLogs || false,
customTransformInput(props) {
if (
props.options.advanced?.database?.useNumberId &&
props.field === "id" &&
props.action === "create"
) {
const useNumberId =
props.options.advanced?.database?.useNumberId ||
props.options.advanced?.database?.generateId === "serial";
if (useNumberId && props.field === "id" && props.action === "create") {
return db[props.model]!.length + 1;
}
return props.data;
@@ -49,7 +48,7 @@ export const memoryAdapter = (
}
},
},
adapter: ({ getFieldName, options, debugLog }) => {
adapter: ({ getFieldName, options, debugLog, getModelName }) => {
function convertWhereClause(where: CleanedWhere[], model: string) {
const table = db[model];
if (!table) {
@@ -117,9 +116,12 @@ export const memoryAdapter = (
}
return {
create: async ({ model, data }) => {
if (options.advanced?.database?.useNumberId) {
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
if (useNumberId) {
// @ts-expect-error
data.id = db[model]!.length + 1;
data.id = db[getModelName(model)]!.length + 1;
}
if (!db[model]) {
db[model] = [];

View File

@@ -30,7 +30,8 @@ const { execute } = await testAdapter({
normalTestSuite(),
authFlowTestSuite(),
transactionsTestSuite(),
// numberIdTestSuite(), // Mongo doesn't support number ids
// numberIdTestSuite(), // no support
// uuidTestSuite() // no support
],
customIdGenerator: () => new ObjectId().toHexString(),
});

View File

@@ -264,6 +264,7 @@ export const prismaAdapter = (prisma: PrismaClient, config: PrismaConfig) => {
adapterName: "Prisma Adapter",
usePlural: config.usePlural ?? false,
debugLogs: config.debugLogs ?? false,
supportsUUIDs: config.provider === "postgresql" ? true : false,
transaction:
(config.transaction ?? false)
? (cb) =>

View File

@@ -6,6 +6,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { prismaAdapter } from "../prisma-adapter";
import { generateAuthConfigFile } from "./generate-auth-config";
@@ -46,6 +47,7 @@ const { execute } = await testAdapter({
transactionsTestSuite(),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite(),
],
onFinish: async () => {},
prefixTests: dialect,

View File

@@ -6,6 +6,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { prismaAdapter } from "../prisma-adapter";
import { generateAuthConfigFile } from "./generate-auth-config";
@@ -44,6 +45,7 @@ const { execute } = await testAdapter({
transactionsTestSuite(),
authFlowTestSuite(),
numberIdTestSuite(),
uuidTestSuite({}),
],
onFinish: async () => {},
prefixTests: "pg",

View File

@@ -7,6 +7,7 @@ import {
normalTestSuite,
numberIdTestSuite,
transactionsTestSuite,
uuidTestSuite,
} from "../../tests";
import { prismaAdapter } from "../prisma-adapter";
import { generateAuthConfigFile } from "./generate-auth-config";
@@ -47,6 +48,7 @@ const { execute } = await testAdapter({
transactionsTestSuite(),
authFlowTestSuite(),
numberIdTestSuite({}),
uuidTestSuite(),
],
onFinish: async () => {},
prefixTests: dialect,

View File

@@ -1069,7 +1069,7 @@ export function runNumberIdAdapterTest(opts: NumberIdAdapterTestOptions) {
await opts.getAdapter({
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
});
@@ -1151,7 +1151,7 @@ export function runNumberIdAdapterTest(opts: NumberIdAdapterTestOptions) {
predefinedOptions: {
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
},

View File

@@ -3,3 +3,4 @@ export * from "./normal";
export * from "./number-id";
export * from "./performance";
export * from "./transactions";
export * from "./uuid";

View File

@@ -11,7 +11,10 @@ export const normalTestSuite = createTestSuite("normal", {}, (helpers) => {
return {
"init - tests": async () => {
const opts = helpers.getBetterAuthOptions();
expect(opts.advanced?.database?.useNumberId).toBe(undefined);
expect(
!opts.advanced?.database?.useNumberId &&
opts.advanced?.database?.generateId !== "serial",
).toBeTruthy();
},
...tests,
};
@@ -37,12 +40,15 @@ export const getNormalTestSuiteTests = ({
forceAllowId: true,
});
const options = getBetterAuthOptions();
if (options.advanced?.database?.useNumberId) {
expect(typeof result.id).toEqual("string");
if (
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial" ||
options.advanced?.database?.generateId === "uuid"
) {
user.id = result.id;
} else {
expect(typeof result.id).toEqual("string");
}
expect(typeof result.id).toEqual("string");
const transformed = transformGeneratedModel(user);
// console.log(`pre-transformed:`, user);
// console.log(`transformed:`, transformed);
@@ -295,9 +301,13 @@ export const getNormalTestSuiteTests = ({
expect(result).toEqual(session);
},
"findOne - should not throw on record not found": async () => {
const options = getBetterAuthOptions();
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const result = await adapter.findOne<User>({
model: "user",
where: [{ field: "id", value: "100000" }],
where: [
{ field: "id", value: useUUIDs ? crypto.randomUUID() : "100000" },
],
});
expect(result).toBeNull();
},
@@ -422,9 +432,13 @@ export const getNormalTestSuiteTests = ({
},
"findMany - should return an empty array when no models are found":
async () => {
const options = getBetterAuthOptions();
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const result = await adapter.findMany<User>({
model: "user",
where: [{ field: "id", value: "100000" }],
where: [
{ field: "id", value: useUUIDs ? crypto.randomUUID() : "100000" },
],
});
expect(result).toEqual([]);
},
@@ -859,7 +873,10 @@ export const getNormalTestSuiteTests = ({
throw error;
}
const options = getBetterAuthOptions();
if (options.advanced?.database?.useNumberId) {
if (
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial"
) {
expect(Number(users[0]!.id)).not.toBeNaN();
}
},
@@ -1166,10 +1183,14 @@ export const getNormalTestSuiteTests = ({
expect(result).toBeNull();
},
"delete - should not throw on record not found": async () => {
const options = getBetterAuthOptions();
const useUUIDs = options.advanced?.database?.generateId === "uuid";
await expect(
adapter.delete({
model: "user",
where: [{ field: "id", value: "100000" }],
where: [
{ field: "id", value: useUUIDs ? crypto.randomUUID() : "100000" },
],
}),
).resolves.not.toThrow();
},

View File

@@ -9,7 +9,7 @@ export const numberIdTestSuite = createTestSuite(
defaultBetterAuthOptions: {
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
},
@@ -23,7 +23,10 @@ export const numberIdTestSuite = createTestSuite(
return {
"init - tests": async () => {
const opts = helpers.getBetterAuthOptions();
expect(opts.advanced?.database?.useNumberId).toBe(true);
expect(
opts.advanced?.database?.useNumberId ||
opts.advanced?.database?.generateId === "serial",
).toBe(true);
},
"create - should return a number id": async () => {
const user = await helpers.generate("user");

View File

@@ -0,0 +1,68 @@
import { expect } from "vitest";
import type { User } from "../../../../core/src/db/schema/user";
import { createTestSuite } from "../create-test-suite";
import { getNormalTestSuiteTests } from "./normal";
export const uuidTestSuite = createTestSuite(
"uuid",
{
defaultBetterAuthOptions: {
advanced: {
database: {
generateId: "uuid",
},
},
},
prefixTests: "uuid",
alwaysMigrate: true,
// This is here to overwrite `generateId` functions to generate UUIDs instead of the default.
// Since existing tests often use generated IDs as well as `forceAllowId` to be true, this is needed to ensure the tests pass.
customIdGenerator() {
return crypto.randomUUID();
},
},
(helpers) => {
const { "create - should use generateId if provided": _, ...normalTests } =
getNormalTestSuiteTests(helpers);
return {
"init - tests": async () => {
const opts = helpers.getBetterAuthOptions();
expect(opts.advanced?.database?.generateId === "uuid").toBe(true);
},
"create - should return a uuid": async () => {
const user = await helpers.generate("user");
const res = await helpers.adapter.create<User>({
model: "user",
data: {
...user,
//@ts-expect-error - remove id from `user`
id: undefined,
},
});
expect(res).toHaveProperty("id");
expect(typeof res.id).toBe("string");
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
expect(res.id).toMatch(uuidRegex);
console.log(res);
},
"findOne - should find a model using a uuid": async () => {
const { id: _, ...user } = await helpers.generate("user");
const res = await helpers.adapter.create<User>({
model: "user",
data: user,
});
const result = await helpers.adapter.findOne<User>({
model: "user",
where: [{ field: "id", value: res.id }],
});
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
expect(result?.id).toMatch(uuidRegex);
expect(result).toEqual(res);
},
...normalTests,
};
},
);

View File

@@ -213,8 +213,10 @@ describe.runIf(isPostgresAvailable)(
},
},
};
const { runMigrations } = await getMigrations(config);
const { runMigrations, compileMigrations } = await getMigrations(config);
await runMigrations();
const migrations = await compileMigrations();
console.log(migrations);
const auth = betterAuth(config);
const user = await auth.api.signUpEmail({
@@ -227,6 +229,12 @@ describe.runIf(isPostgresAvailable)(
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
expect(user.user.id).toMatch(uuidRegex);
// run migrations again to ensure no migrations are needed & no errors are thrown
const { compileMigrations: round2Migrations } =
await getMigrations(config);
const secondRoundOfMigrations = await round2Migrations();
expect(secondRoundOfMigrations).toEqual(";");
});
},
);

View File

@@ -317,15 +317,30 @@ export async function getMigrations(config: BetterAuthOptions) {
},
id: {
postgres: useNumberId ? "serial" : useUUIDs ? "uuid" : "text",
mysql: useNumberId ? "integer" : useUUIDs ? "uuid" : "varchar(36)",
mssql: useNumberId ? "integer" : useUUIDs ? "uuid" : "varchar(36)",
mysql: useNumberId
? "integer"
: useUUIDs
? "varchar(36)"
: "varchar(36)",
mssql: useNumberId
? "integer"
: useUUIDs
? "varchar(36)"
: "varchar(36)",
sqlite: useNumberId ? "integer" : "text",
},
foreignKeyId: {
postgres: useNumberId ? "integer" : useUUIDs ? "uuid" : "text",
mysql: useNumberId ? "integer" : useUUIDs ? "uuid" : "varchar(36)",
mssql: useNumberId ? "integer" : useUUIDs ? "uuid" : "varchar(36)",
mysql: useNumberId
? "integer"
: useUUIDs
? "varchar(36)"
: "varchar(36)",
mssql: useNumberId
? "integer"
: useUUIDs
? "varchar(36)" /* Should be using `UNIQUEIDENTIFIER` but Kysely doesn't support it */
: "varchar(36)",
sqlite: useNumberId ? "integer" : "text",
},
} as const;
@@ -399,21 +414,10 @@ export async function getMigrations(config: BetterAuthOptions) {
if (toBeCreated.length) {
for (const table of toBeCreated) {
let dbT = db.schema.createTable(table.table).addColumn(
"id",
useNumberId
? dbType === "postgres"
? "serial"
: "integer"
: useUUIDs
? dbType === "postgres" || dbType === "mysql" || dbType === "mssql"
? "uuid"
: "text"
: dbType === "mysql" || dbType === "mssql"
? "varchar(36)"
: "text",
(col) => {
const idType = getType({ type: useNumberId ? "number" : "string" }, "id");
let dbT = db.schema
.createTable(table.table)
.addColumn("id", idType, (col) => {
if (useNumberId) {
if (dbType === "postgres" || dbType === "sqlite") {
return col.primaryKey().notNull();
@@ -428,14 +432,11 @@ export async function getMigrations(config: BetterAuthOptions) {
.primaryKey()
.defaultTo(sql`pg_catalog.gen_random_uuid()`)
.notNull();
} else if (dbType === "mysql" || dbType === "mssql") {
return col.primaryKey().defaultTo(sql`uuid()`).notNull();
}
return col.primaryKey().notNull();
}
return col.primaryKey().notNull();
},
);
});
for (const [fieldName, field] of Object.entries(table.fields)) {
const type = getType(field, fieldName);

View File

@@ -52,7 +52,11 @@ export const generateDrizzleSchema: SchemaGenerator = async ({
}
name = convertToSnakeCase(name, adapter.options?.camelCase);
if (field.references?.field === "id") {
if (options.advanced?.database?.useNumberId) {
const useNumberId =
options.advanced?.database?.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") {
@@ -62,6 +66,9 @@ export const generateDrizzleSchema: SchemaGenerator = async ({
return `integer('${name}')`;
}
}
if (useUUIDs && databaseType === "pg") {
return `uuid('${name}')`;
}
if (field.references.field) {
if (databaseType === "mysql") {
return `varchar('${name}', { length: 36 })`;
@@ -144,7 +151,14 @@ export const generateDrizzleSchema: SchemaGenerator = async ({
let id: string = "";
if (options.advanced?.database?.useNumberId) {
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (useUUIDs && databaseType === "pg") {
id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
} else if (useNumberId) {
if (databaseType === "pg") {
id = `serial("id").primaryKey()`;
} else if (databaseType === "sqlite") {
@@ -289,7 +303,11 @@ function generateImport({
if (hasJson && hasBigint) break;
}
const useNumberId = options.advanced?.database?.useNumberId;
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
coreImports.push(`${databaseType}Table`);
coreImports.push(
@@ -328,6 +346,10 @@ function generateImport({
coreImports.push("mysqlEnum");
}
} else if (databaseType === "pg") {
if (useUUIDs) {
rootImports.push("sql");
}
// Only include integer for PG if actually needed
const hasNonBigintNumber = Object.values(tables).some((table) =>
Object.values(table.fields).some(
@@ -344,14 +366,22 @@ function generateImport({
// handles the references field with useNumberId
const needsInteger =
hasNonBigintNumber ||
(options.advanced?.database?.useNumberId && hasFkToId);
((options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial") &&
hasFkToId);
if (needsInteger) {
coreImports.push("integer");
}
} else {
coreImports.push("integer");
}
coreImports.push(useNumberId ? (databaseType === "pg" ? "serial" : "") : "");
if (databaseType === "pg") {
if (useNumberId) {
coreImports.push("serial");
} else if (useUUIDs) {
coreImports.push("uuid");
}
}
//handle json last on the import order
if (hasJson) {

View File

@@ -124,12 +124,25 @@ export const generatePrismaSchema: SchemaGenerator = async ({
.attribute("id")
.attribute(`map("_id")`);
} else {
if (options.advanced?.database?.useNumberId) {
const useNumberId =
options.advanced?.database?.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("db.Uuid")
.attribute(
'default(dbgenerated("pg_catalog.gen_random_uuid()"))',
);
} else {
builder.model(modelName).field("id", "String").attribute("id");
}
@@ -149,10 +162,13 @@ export const generatePrismaSchema: SchemaGenerator = async ({
continue;
}
}
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const useNumberId =
options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial";
const fieldBuilder = builder.model(modelName).field(
fieldName,
field === "id" && options.advanced?.database?.useNumberId
field === "id" && useNumberId
? getType({
isBigint: false,
isOptional: false,
@@ -163,7 +179,7 @@ export const generatePrismaSchema: SchemaGenerator = async ({
isOptional: !attr?.required,
type:
attr.references?.field === "id"
? options.advanced?.database?.useNumberId
? useNumberId
? "number"
: "string"
: attr.type,
@@ -202,6 +218,10 @@ export const generatePrismaSchema: SchemaGenerator = async ({
}
if (attr.references) {
if (useUUIDs && provider === "postgresql") {
fieldBuilder.attribute(`db.Uuid`);
}
const referencedOriginalModelName = attr.references.model;
const referencedCustomModelName =
tables[referencedOriginalModelName]?.modelName ||
@@ -212,17 +232,17 @@ export const generatePrismaSchema: SchemaGenerator = async ({
else if (attr.references.onDelete === "set default")
action = "SetDefault";
else if (attr.references.onDelete === "restrict") action = "Restrict";
const relationField = `relation(fields: [${fieldName}], references: [${attr.references.field}], onDelete: ${action})`;
builder
.model(modelName)
.field(
`${referencedCustomModelName.toLowerCase()}`,
referencedCustomModelName.toLowerCase(),
`${capitalizeFirstLetter(referencedCustomModelName)}${
!attr.required ? "?" : ""
}`,
)
.attribute(
`relation(fields: [${fieldName}], references: [${attr.references.field}], onDelete: ${action})`,
);
.attribute(relationField);
}
if (
!attr.unique &&
@@ -258,10 +278,11 @@ export const generatePrismaSchema: SchemaGenerator = async ({
let indexField = fieldName;
if (provider === "mysql" && field && field.type === "string") {
if (
field.references?.field === "id" &&
options.advanced?.database?.useNumberId
) {
const useNumberId =
options.advanced?.database?.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)`; // length of 191 because String in Prisma is varchar(191)

View File

@@ -0,0 +1,99 @@
import {
mysqlTable,
varchar,
text,
timestamp,
boolean,
index,
} from "drizzle-orm/mysql-core";
export const custom_user = mysqlTable("custom_user", {
id: varchar("id", { length: 36 }).primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
twoFactorEnabled: boolean("two_factor_enabled").default(false),
username: varchar("username", { length: 255 }).unique(),
displayUsername: text("display_username"),
});
export const custom_session = mysqlTable(
"custom_session",
{
id: varchar("id", { length: 36 }).primaryKey(),
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
token: varchar("token", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [index("custom_session_userId_idx").on(table.userId)],
);
export const custom_account = mysqlTable(
"custom_account",
{
id: varchar("id", { length: 36 }).primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_account_userId_idx").on(table.userId)],
);
export const custom_verification = mysqlTable(
"custom_verification",
{
id: varchar("id", { length: 36 }).primaryKey(),
identifier: varchar("identifier", { length: 255 }).notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at", { fsp: 3 }).notNull(),
createdAt: timestamp("created_at", { fsp: 3 }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { fsp: 3 })
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_verification_identifier_idx").on(table.identifier)],
);
export const twoFactor = mysqlTable(
"two_factor",
{
id: varchar("id", { length: 36 }).primaryKey(),
secret: varchar("secret", { length: 255 }).notNull(),
backupCodes: text("backup_codes").notNull(),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [
index("twoFactor_secret_idx").on(table.secret),
index("twoFactor_userId_idx").on(table.userId),
],
);

View File

@@ -0,0 +1,110 @@
import { sql } from "drizzle-orm";
import {
pgTable,
text,
timestamp,
boolean,
uuid,
index,
} from "drizzle-orm/pg-core";
export const custom_user = pgTable("custom_user", {
id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
twoFactorEnabled: boolean("two_factor_enabled").default(false),
username: text("username").unique(),
displayUsername: text("display_username"),
});
export const custom_session = pgTable(
"custom_session",
{
id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: uuid("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [index("custom_session_userId_idx").on(table.userId)],
);
export const custom_account = pgTable(
"custom_account",
{
id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: uuid("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_account_userId_idx").on(table.userId)],
);
export const custom_verification = pgTable(
"custom_verification",
{
id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_verification_identifier_idx").on(table.identifier)],
);
export const twoFactor = pgTable(
"two_factor",
{
id: uuid("id")
.default(sql`pg_catalog.gen_random_uuid()`)
.primaryKey(),
secret: text("secret").notNull(),
backupCodes: text("backup_codes").notNull(),
userId: uuid("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [
index("twoFactor_secret_idx").on(table.secret),
index("twoFactor_userId_idx").on(table.userId),
],
);

View File

@@ -0,0 +1,109 @@
import { sql } from "drizzle-orm";
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const custom_user = sqliteTable("custom_user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" })
.default(false)
.notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
twoFactorEnabled: integer("two_factor_enabled", { mode: "boolean" }).default(
false,
),
username: text("username").unique(),
displayUsername: text("display_username"),
});
export const custom_session = sqliteTable(
"custom_session",
{
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [index("custom_session_userId_idx").on(table.userId)],
);
export const custom_account = sqliteTable(
"custom_account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", {
mode: "timestamp_ms",
}),
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
mode: "timestamp_ms",
}),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_account_userId_idx").on(table.userId)],
);
export const custom_verification = sqliteTable(
"custom_verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("custom_verification_identifier_idx").on(table.identifier)],
);
export const twoFactor = sqliteTable(
"two_factor",
{
id: text("id").primaryKey(),
secret: text("secret").notNull(),
backupCodes: text("backup_codes").notNull(),
userId: text("user_id")
.notNull()
.references(() => custom_user.id, { onDelete: "cascade" }),
},
(table) => [
index("twoFactor_secret_idx").on(table.secret),
index("twoFactor_userId_idx").on(table.userId),
],
);

View File

@@ -0,0 +1,89 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @db.Uuid @default(dbgenerated("pg_catalog.gen_random_uuid()"))
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
twoFactorEnabled Boolean? @default(false)
username String?
displayUsername String?
sessions Session[]
accounts Account[]
twofactors TwoFactor[]
@@unique([email])
@@unique([username])
@@map("user")
}
model Session {
id String @id @db.Uuid @default(dbgenerated("pg_catalog.gen_random_uuid()"))
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@index([userId])
@@map("session")
}
model Account {
id String @id @db.Uuid @default(dbgenerated("pg_catalog.gen_random_uuid()"))
accountId String
providerId String
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@map("account")
}
model Verification {
id String @id @db.Uuid @default(dbgenerated("pg_catalog.gen_random_uuid()"))
identifier String
value String
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([identifier])
@@map("verification")
}
model TwoFactor {
id String @id @db.Uuid @default(dbgenerated("pg_catalog.gen_random_uuid()"))
secret String
backupCodes String
userId String @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([secret])
@@index([userId])
@@map("twoFactor")
}

View File

@@ -103,7 +103,7 @@ describe("generate drizzle schema for all databases", async () => {
plugins: [twoFactor(), username()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
user: {
@@ -124,6 +124,133 @@ describe("generate drizzle schema for all databases", async () => {
"./__snapshots__/auth-schema-mysql-number-id.txt",
);
});
it("should generate drizzle schema for MySQL with uuid id", async () => {
const schema = await generateDrizzleSchema({
file: "test.drizzle",
adapter: drizzleAdapter(
{},
{
provider: "mysql",
schema: {},
},
)({} as BetterAuthOptions),
options: {
database: drizzleAdapter(
{},
{
provider: "mysql",
schema: {},
},
),
plugins: [twoFactor(), username()],
advanced: {
database: {
generateId: "uuid",
},
},
user: {
modelName: "custom_user",
},
account: {
modelName: "custom_account",
},
session: {
modelName: "custom_session",
},
verification: {
modelName: "custom_verification",
},
},
});
expect(schema.code).toMatchFileSnapshot(
"./__snapshots__/auth-schema-mysql-uuid.txt",
);
});
it("should generate drizzle schema for PostgreSQL with uuid 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: "uuid",
},
},
user: {
modelName: "custom_user",
},
account: {
modelName: "custom_account",
},
session: {
modelName: "custom_session",
},
verification: {
modelName: "custom_verification",
},
},
});
expect(schema.code).toMatchFileSnapshot(
"./__snapshots__/auth-schema-pg-uuid.txt",
);
});
it("should generate drizzle schema for SQLite with uuid id", async () => {
const schema = await generateDrizzleSchema({
file: "test.drizzle",
adapter: drizzleAdapter(
{},
{
provider: "sqlite",
schema: {},
},
)({} as BetterAuthOptions),
options: {
database: drizzleAdapter(
{},
{
provider: "sqlite",
schema: {},
},
),
plugins: [twoFactor(), username()],
advanced: {
database: {
generateId: "uuid",
},
},
user: {
modelName: "custom_user",
},
account: {
modelName: "custom_account",
},
session: {
modelName: "custom_session",
},
verification: {
modelName: "custom_verification",
},
},
});
expect(schema.code).toMatchFileSnapshot(
"./__snapshots__/auth-schema-sqlite-uuid.txt",
);
});
it("should generate drizzle schema for SQLite with number id", async () => {
const schema = await generateDrizzleSchema({
@@ -146,7 +273,7 @@ describe("generate drizzle schema for all databases", async () => {
plugins: [twoFactor(), username()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
user: {
@@ -305,7 +432,7 @@ describe("generate drizzle schema for all databases with passkey plugin", async
plugins: [passkey()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
user: {
@@ -348,7 +475,7 @@ describe("generate drizzle schema for all databases with passkey plugin", async
plugins: [passkey()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
user: {

View File

@@ -52,7 +52,7 @@ describe("generate", async () => {
plugins: [twoFactor(), username()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
},
@@ -62,6 +62,35 @@ describe("generate", async () => {
);
});
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",
},
},
},
});
expect(schema.code).toMatchFileSnapshot(
"./__snapshots__/schema-uuid.prisma",
);
});
it("should generate prisma schema for mongodb", async () => {
const schema = await generatePrismaSchema({
file: "test.prisma",
@@ -204,7 +233,7 @@ describe("generate", async () => {
plugins: [twoFactor(), username()],
advanced: {
database: {
useNumberId: true,
generateId: "serial",
},
},
user: {

View File

@@ -82,6 +82,12 @@ export interface DBAdapterFactoryConfig<
* @default true
*/
supportsNumericIds?: boolean | undefined;
/**
* If the database supports natively generating UUIDs, set this to `true`.
*
* @default false
*/
supportsUUIDs?: boolean | undefined;
/**
* If the database doesn't support JSON columns, set this to `false`.
*
@@ -246,7 +252,7 @@ export interface DBAdapterFactoryConfig<
*
*
* Notes:
* - If the user enabled `useNumberId`, then this option will be ignored. Unless this adapter config has `supportsNumericIds` set to `false`.
* - If the user enabled `useNumberId` or `generateId` set to `serial`, then this option will be ignored. Unless this adapter config has `supportsNumericIds` set to `false`.
* - If `generateId` is `false` in the user's Better-Auth config, then this option will be ignored.
* - If `generateId` is a function, then it will override this option.
*

View File

@@ -117,7 +117,9 @@ export function getTelemetryAuthConfig(
options.advanced?.crossSubDomainCookies?.additionalCookies,
},
database: {
useNumberId: !!options.advanced?.database?.useNumberId,
useNumberId:
!!options.advanced?.database?.useNumberId ||
options.advanced?.database?.generateId === "serial",
generateId: options.advanced?.database?.generateId,
defaultFindManyLimit: options.advanced?.database?.defaultFindManyLimit,
},