mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
fix: treat generateId "serial" as numeric ID and correct UUID column types across adapters (#5823)
This commit is contained in:
@@ -22,6 +22,7 @@ exports[`init > should match config 1`] = `
|
||||
"supportsDates": false,
|
||||
"supportsJSON": false,
|
||||
"supportsNumericIds": true,
|
||||
"supportsUUIDs": false,
|
||||
"transaction": false,
|
||||
"usePlural": undefined,
|
||||
},
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
|
||||
@@ -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"> {
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {},
|
||||
});
|
||||
|
||||
@@ -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] = [];
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./normal";
|
||||
export * from "./number-id";
|
||||
export * from "./performance";
|
||||
export * from "./transactions";
|
||||
export * from "./uuid";
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
68
packages/better-auth/src/adapters/tests/uuid.ts
Normal file
68
packages/better-auth/src/adapters/tests/uuid.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -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(";");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
99
packages/cli/test/__snapshots__/auth-schema-mysql-uuid.txt
Normal file
99
packages/cli/test/__snapshots__/auth-schema-mysql-uuid.txt
Normal 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),
|
||||
],
|
||||
);
|
||||
110
packages/cli/test/__snapshots__/auth-schema-pg-uuid.txt
Normal file
110
packages/cli/test/__snapshots__/auth-schema-pg-uuid.txt
Normal 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),
|
||||
],
|
||||
);
|
||||
109
packages/cli/test/__snapshots__/auth-schema-sqlite-uuid.txt
Normal file
109
packages/cli/test/__snapshots__/auth-schema-sqlite-uuid.txt
Normal 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),
|
||||
],
|
||||
);
|
||||
89
packages/cli/test/__snapshots__/schema-uuid.prisma
Normal file
89
packages/cli/test/__snapshots__/schema-uuid.prisma
Normal 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")
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user