创建数据库适配器
了解如何为 Better-Auth 创建自定义数据库适配器
了解如何使用 createAdapterFactory 为 Better-Auth 创建自定义数据库适配器。
我们的 createAdapterFactory 函数设计得非常灵活,我们尽力让它易于理解和使用。
我们希望您可以专注于编写数据库逻辑,而无需担心适配器如何与 Better-Auth 配合工作。
从自定义模式配置、自定义 ID 生成、安全 JSON 解析、键映射、连接查询等,都由 createAdapterFactory 函数处理。
您只需提供数据库逻辑,createAdapterFactory 函数将处理剩余工作。
快速开始
准备工作
- 导入
createAdapterFactory。 - 创建
CustomAdapterConfig接口,代表您的适配器配置选项。 - 创建适配器!
import { createAdapterFactory, type DBAdapterDebugLogOption } from "better-auth/adapters";
// 您的自定义适配器配置选项
interface CustomAdapterConfig {
/**
* 帮助您调试适配器问题。
*/
debugLogs?: DBAdapterDebugLogOption;
/**
* 架构中的表名是否为复数形式。
*/
usePlural?: boolean;
}
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapterFactory({
// ...
});配置适配器
config 对象主要用于向 Better-Auth 提供适配器相关信息。
我们尽量减少在适配器函数中需要编写的代码量,这些 config 选项用于帮助我们实现这一点。
// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapterFactory({
config: {
adapterId: "custom-adapter", // 适配器的唯一标识符。
adapterName: "Custom Adapter", // 适配器的名称。
usePlural: config.usePlural ?? false, // 架构中的表名是否为复数。
debugLogs: config.debugLogs ?? false, // 是否启用调试日志。
supportsJSON: false, // 数据库是否支持 JSON。(默认:false)
supportsDates: true, // 数据库是否支持日期。(默认:true)
supportsBooleans: true, // 数据库是否支持布尔值。(默认:true)
supportsNumericIds: true, // 数据库是否支持自动递增的数字 ID。(默认:true)
},
// ...
});创建适配器
adapter 函数是您编写与数据库交互代码的地方。
// ...
export const myAdapter = (config: CustomAdapterConfig = {}) =>
createAdapterFactory({
config: {
// ...
},
adapter: ({}) => {
return {
create: async ({ data, model, select }) => {
// ...
},
update: async ({ model, where, update }) => {
// ...
},
updateMany: async ({ model, where, update }) => {
// ...
},
delete: async ({ model, where }) => {
// ...
},
// ...
};
},
});了解更多关于数据库适配器的信息。
适配器
adapter 函数是您编写与数据库交互代码的地方。
如果您还没有,建议先查看配置部分中的 options 对象,它对您的适配器可能非常有用。
在深入适配器函数之前,让我们先了解可用的参数。
options: Better Auth 选项。schema: 来自用户 Better Auth 实例的架构。debugLog: 调试日志函数。getModelName: 用于获取数据库中转换后模型名称的函数。getFieldName: 用于获取数据库中转换后字段名称的函数。getDefaultModelName: 用于从架构中获取默认模型名称的函数。getDefaultFieldName: 用于从架构中获取默认字段名称的函数。getFieldAttributes: 用于获取特定模型和字段的字段属性的函数。transformInput: 用于在保存到数据库之前转换输入数据的函数。transformOutput: 用于从数据库检索后转换输出数据的函数。transformWhereClause: 用于转换数据库查询where子句的函数。
adapter: ({
options,
schema,
debugLog,
getModelName,
getFieldName,
getDefaultModelName,
getDefaultFieldName,
getFieldAttributes,
transformInput,
transformOutput,
transformWhereClause,
}) => {
return {
// ...
};
};适配器方法
- 所有
model值都会根据用户的架构配置自动转换为正确的数据库模型名称。- 这也意味着,如果您需要访问给定模型的
schema版本,则不能直接使用此model值,您需要使用getDefaultModelName函数将model转换为schema版本。
- 这也意味着,如果您需要访问给定模型的
- 我们会根据用户的
schema配置自动填充您返回中缺失的任何字段。 - 任何包含
select参数的方法仅用于更高效地从数据库获取数据。您不需要担心只返回select参数指定的内容,因为我们会为您处理。
create 方法
create 方法用于在数据库中创建新记录。
可以将 forceAllowId 作为参数传递给 create 方法,这允许在 data 对象中提供 id。
我们在内部处理 forceAllowId,因此您不需要担心它。
参数:
model: 新数据将插入的模型/表名。data: 要插入数据库的数据。select: 要从数据库返回的字段数组。
Returns Promise<T>: 插入到数据库中的记录。
create: async ({ data, model, select }) => {
// 插入数据的示例
return await db.insert(model).values(data);
};update 方法
update 方法用于更新数据库中的一条记录。
参数:
model: 记录将更新的模型/表名。where: 用于更新记录的where子句。update: 用于更新记录的数据。
Returns Promise<T | null>: 更新后的行,如果没有行匹配则为 null。对于多个 where 子句,某些适配器无法返回它。
update: async ({ model, where, update }) => {
// 更新数据的示例
return await db.update(model).set(update).where(where);
};updateMany 方法
updateMany 方法用于批量更新数据库中的多条记录。
参数:
model: 记录将更新的模型/表名。where: 用于更新记录的where子句。update: 用于更新记录的数据。
Returns Promise<number>: 已更新的记录数量。
updateMany: async ({ model, where, update }) => {
// 批量更新数据的示例
return await db.update(model).set(update).where(where);
};delete 方法
delete 方法用于删除数据库中的一条记录。
参数:
model: 记录将从其中删除的模型/表名。where: 用于删除记录的where子句。
Returns Promise<void>: 无返回值。
delete: async ({ model, where }) => {
// 删除记录的示例
await db.delete(model).where(where);
}deleteMany 方法
deleteMany 方法用于批量删除数据库中的多条记录。
参数:
model: 记录将从其中删除的模型/表名。where: 用于删除记录的where子句。
Returns Promise<number>: 已删除的记录数量。
deleteMany: async ({ model, where }) => {
// 批量删除记录的示例
return await db.delete(model).where(where);
};consumeOne 方法(可选)
consumeOne 方法会以原子方式删除一条匹配的记录并返回它,从而确保两个并发请求不可能同时消费同一行。它用于一次性凭证,例如一次性验证挑战。实现它是可选的:如果省略,适配器工厂会回退为在 transaction 中包装 findMany + deleteMany,并使用已删除行数作为竞态门控。
参数:
model: 要从中消费记录的模型/表名。where: 用于匹配单条记录的where子句。
Returns Promise<T | null>: 已消费的记录;如果没有匹配到任何行(包括另一请求已先一步消费时)则为 null。实现时最多只能删除一条匹配行。
consumeOne: async ({ model, where }) => {
// 单条 DELETE ... RETURNING 是原子的,因此只有一个调用者能拿到该行。
const [row] = await db.delete(model).where(where).returning();
return row ?? null;
};尽管工厂提供了回退方案,仍然建议原生实现 consumeOne:该回退方案只有在真实 transaction 且删除计数准确时才具备抗竞争条件的安全性。因此,没有多文档事务的存储需要原生的原子删除并返回能力(DELETE ... RETURNING、findOneAndDelete,或基于版本条件的删除)。
findOne 方法
findOne 方法用于查找数据库中的单条记录。
参数:
model: 记录将查找的模型/表名。where: 用于查找记录的where子句。select: 要返回的选择子句。join: 可选的连接配置,用于在一个查询中获取相关记录。
Returns Promise<T | null>: 匹配的记录,如果未找到则为 null。
findOne: async ({ model, where, select, join }) => {
// 查找单条记录的示例
return await db.select().from(model).where(where).limit(1);
};findMany 方法
findMany 方法用于查找数据库中的多条记录。
参数:
model: 记录将查找的模型/表名。where: 用于查找记录的where子句。limit: 返回记录的上限。select: 要返回的select子句。sortBy: 用于对记录排序的sortBy子句。offset: 返回记录的偏移量。join: 可选的连接配置,用于在一个查询中获取相关记录。
Returns Promise<T[]>: 匹配记录的数组。
findMany: async ({ model, where, limit, select, sortBy, offset, join }) => {
// 在数据库中查找多条记录的示例。
return await db
.select()
.from(model)
.where(where)
.limit(limit)
.offset(offset)
.orderBy(sortBy);
};count 方法
count 方法用于统计数据库中记录的数量。
参数:
model: 记录将统计的模型/表名。where: 用于统计记录的where子句。
Returns Promise<number>: 被统计的记录数量。
count: async ({ model, where }) => {
// 统计记录数的示例
return await db.select().from(model).where(where).count();
};options(可选)
options 对象用于传递自定义适配器配置中可能包含的任何配置。
const myAdapter = (config: CustomAdapterConfig) =>
createAdapterFactory({
config: {
// ...
},
adapter: ({ options }) => {
return {
options: config,
};
},
});createSchema(可选)
createSchema 方法允许 Better Auth CLI 为数据库生成模式。
参数:
tables: 用户 Better-Auth 实例架构中的表;期望生成到架构文件中。file: 用户在generate命令中可能传入的文件,作为预期的架构文件输出路径。
createSchema: async ({ file, tables }) => {
// ... 为数据库自定义生成模式的逻辑。
};测试您的适配器
我们提供了一个通过 @better-auth/test-utils 包的测试套件,您可以用它来测试您的适配器。它需要您使用 vitest。
首先安装测试工具:
npm install -D @better-auth/test-utils然后使用 testAdapter 和 createTestSuite 创建测试文件:
import { testAdapter, createTestSuite } from "@better-auth/test-utils/adapter";
import { myAdapter } from "./my-adapter";
const normalTestSuite = createTestSuite("Normal", ({ test, adapter }) => [
test("should create and find a user", async () => {
// 在这里使用 adapter 实例编写您的适配器测试
}),
]);
const { execute } = await testAdapter({
adapter: (options) => {
return myAdapter(/* 您的适配器配置 */);
},
runMigrations: async (options) => {
// 在这里运行数据库迁移
},
tests: [
normalTestSuite(),
],
async onFinish() {
// 可选:所有测试完成后的清理(例如删除数据库文件)
},
});
execute();testAdapter 函数为您处理测试生命周期,包括在测试前运行迁移和测试完成后清理所有表。
在 v1.4 版本中,适配器测试使用了 better-auth/adapters/test 中的 runAdapterTest 和 runNumberIdAdapterTest,该导出在 v1.5 中已移除。请改用 @better-auth/test-utils/adapter。
配置
config 对象用于向 Better-Auth 提供关于适配器的信息。
我们强烈建议认真阅读以下各项配置选项,它们将帮助你正确配置适配器。
必需配置
adapterId
适配器的唯一标识符。
adapterName
适配器的名称。
可选配置
supportsNumericIds
数据库是否支持数字 ID。如果设为 false,而用户配置启用了 useNumberId,则会抛出错误。
supportsUUIDs
数据库是否可以原生生成 UUID。默认为 false。如果你的数据库会自行生成 UUID,请将此项设为 true。
supportsJSON
数据库是否支持 JSON。如果不支持,我们将使用字符串保存 JSON 数据,并在取出时安全地将字符串解析回 JSON 对象。
supportsDates
数据库是否支持日期。如果不支持,我们将把日期保存为字符串(ISO 字符串),取出时安全地解析回 Date 对象。
supportsBooleans
数据库是否支持布尔值。如果不支持,将使用 0 或 1 保存布尔值,取出时安全解析回布尔值。
supportsArrays
数据库是否支持数组。默认为 false,在这种情况下,数组字段(string[]、number[])会以序列化后的 string 形式存储,并在读取时安全地解析回来。如果你的数据库原生支持数组列,请将此项设为 true。
usePlural
模式中的表名是否为复数形式。通常由用户定义,并通过你的自定义适配器选项传递。如果你不打算允许用户自定义表名,可以忽略此选项或设为 false。
const adapter = myAdapter({
// 该值会传递至 createAdapterFactory 的 `config` 对象中的 `usePlural`
usePlural: true,
});transaction
适配器是否支持事务。如果为 false,操作顺序执行;否则需提供一个函数,用于执行带 TransactionAdapter 的回调。
如果你的数据库不支持事务,错误处理和回滚不会那么健壮。建议使用支持事务的数据库以保证数据完整性。
debugLogs
启用适配器的调试日志。你可以传入布尔值,或一个包含 create、update、updateMany、findOne、findMany、delete、deleteMany、count 等键的对象。任何键值为 true 的方法会启用调试日志。
// 为所有方法启用调试日志。
const adapter = myAdapter({
debugLogs: true,
});// 仅为 `create` 和 `update` 方法启用调试日志。
const adapter = myAdapter({
debugLogs: {
create: true,
update: true,
},
});disableIdGeneration
是否禁用 ID 生成。如果设为 true,则忽略用户的 generateId 选项。
customIdGenerator
如果你的数据库只支持特定的自定义 ID 生成,可以使用此选项生成自己的 ID。
mapKeysTransformInput
如果数据库在某些情况下使用不同的键名,可以用此选项映射键名。适用于数据库期望某些键名不同的情况。
例如,MongoDB 使用 _id,而 Better-Auth 使用 id。
返回对象中的每个键表示待替换的旧键,值为新键。
可以是部分对象,仅转换部分键。
mapKeysTransformInput: {
id: "_id", // 我们希望将 `id` 替换为 `_id` 以保存进 MongoDB
},mapKeysTransformOutput
如果数据库在某些情况下使用不同的键名,可以用此选项映射键名。适用于数据库使用不同键名的场景。
例如,MongoDB 使用 _id,而 Better-Auth 使用 id。
返回对象中的每个键表示待替换的旧键,值为新键。
可以是部分对象,仅转换部分键。
mapKeysTransformOutput: {
_id: "id", // 我们希望将 `_id`(来自 MongoDB)替换为 `id`(供 Better-Auth 使用)
},customTransformInput
如果需要在数据保存到数据库前转换输入数据,可以使用此选项。
如果你启用了 supportsJSON、supportsDates 或 supportsBooleans,这些转换会在调用你的 customTransformInput 函数之前执行。
customTransformInput 函数接收以下参数:
data: 需要转换的数据。field: 当前转换的字段。fieldAttributes: 该字段的属性。action: 调用的适配器动作(create或update)。model: 当前模型。schema: 当前模式。options: Better Auth 选项。
data:需转换的数据。field:当前转换的字段。fieldAttributes:该字段的属性。action:调用的适配器动作(create或update)。model:当前模型。schema:当前模式。options:Better Auth 选项。
customTransformInput 函数在给定动作的数据对象的每个键上运行。
customTransformInput: ({ field, data }) => {
if (field === "id") {
return "123"; // 强制 ID 为 "123"
}
return data;
};customTransformOutput
如果需要在数据返回给用户前转换输出数据,可以使用此选项。
它和 customTransformInput 类似,但在数据从数据库取出后执行。
customTransformOutput: ({ field, data }) => {
if (field === "name") {
return "Bob"; // 强制姓名为 "Bob"
}
return data;
};const some_data = await adapter.create({
model: "user",
data: {
name: "John",
},
});
// 姓名将会是 "Bob"
console.log(some_data.name);disableTransformInput
是否禁用输入转换。只有在你明确知道自己在做什么,并手动处理所有转换时才应禁用。
禁用输入转换可能导致重要的适配器功能失效,例如 ID 生成、布尔/日期/JSON 转换和键映射。
disableTransformOutput
是否禁用输出转换。只有在你明确知道自己在做什么,并手动处理所有转换时才应禁用。
禁用输出转换可能导致重要的适配器功能失效,例如布尔/日期/JSON 解析和键映射。
disableTransformJoin
是否禁用连接转换。只有在你明确知道自己在做什么,并手动处理连接查询时才应禁用。
禁用连接转换可能导致连接功能失效。