Auth.js EdgeDB Adapter 完整实战指南:安装配置、ESDL 数据模型与数据库会话存储
【免费下载链接】next-authAuthentication for the Web.项目地址: https://gitcode.com/gh_mirrors/ne/next-auth
导读
本篇指南以 packages/adapter-edgedb 官方适配器为主体,讲解如何在 Auth.js / NextAuth.js 项目中接入 EdgeDB 数据库:从依赖安装、环境变量、多框架配置,到 EdgeDB CLI 初始化、default.esdl数据模型定义、迁移与查询构建器生成,再到生产环境部署的完整链路。读完后你将掌握EdgeDBAdapter(client)的接入方式、它与@auth/core中Adapter接口的对应关系,以及底层每条 EdgeQL 语句的实际实现细节,能够直接在自己的项目里落地一套以数据库为核心的认证会话存储方案。
一、EdgeDB Adapter 是什么
Auth.js 的数据库适配器机制定义在 packages/core/src/adapters.ts:当你在AuthConfig中设置strategy: "database"时,Auth.js 不再把会话放在 JWT 里,而是通过Adapter接口把用户、账号、会话、验证令牌的读写操作映射到任意数据层。@auth/edgedb-adapter正是这套机制在 EdgeDB 上的官方实现。
在 packages/adapter-edgedb/src/index.ts 中可以看到其入口签名:
export function EdgeDBAdapter(client: Client): Adapter它接收一个 EdgeDB 官方 JS 客户端(edgedb包的Client实例),返回一个符合Adapter接口的对象。这种「工厂函数 + 复用外部 client」的模式,与@auth/core/adapters中推荐的官方适配器写法完全一致,意味着你可以自行控制连接配置(如 DSN、TLS、连接池),并把同一个 client 复用到应用的其他数据访问逻辑中。
二、安装依赖
安装适配器本身以及 EdgeDB 客户端:
npm install edgedb @auth/edgedb-adapter npm install @edgedb/generate --save-devedgedb:官方 TypeScript 客户端,是适配器的 peer dependency。从 packages/adapter-edgedb/package.json 可以看到@auth/edgedb-adapter声明的 peer 依赖为edgedb ^1.0.1,且仅依赖@auth/core(workspace 引用),本身零运行时数据库依赖,非常轻量。@edgedb/generate:开发期依赖,用于生成类型安全的 EdgeQL 查询构建器(见下文「生成」一节)。
安装后,包名@auth/edgedb-adapter在 package.json 中登记,其导出入口为./index.js+./index.d.ts,直接import { EdgeDBAdapter } from "@auth/edgedb-adapter"即可使用。
三、环境变量
适配器本身不读环境变量,但官方示例约定使用AUTH_EDGEDB_DSN存放 EdgeDB 连接串:
AUTH_EDGEDB_DSN="edgedb://edgedb:p4ssw0rd@10.0.0.1"DSN(连接字符串)的标准格式为edgedb://username:password@hostname:port。在本地开发时,由于edgedb project init会把实例与当前目录「链接」,客户端可以不传 DSN 自动连接;在部署环境中则必须显式提供。
四、各框架的接入配置
4.1 Next.js(App Router)
在项目根目录的auth.ts中创建客户端并传入适配器:
import NextAuth from "next-auth" import { EdgeDBAdapter } from "@auth/edgedb-adapter" import { createClient } from "edgedb" const client = createClient({ dsn: process.env.AUTH_EDGEDB_DSN }) export const { handlers, auth, signIn, signOut } = NextAuth({ adapter: EdgeDBAdapter(client), providers: [], })4.2 Qwik
import { QwikAuth$ } from "@auth/qwik" import { EdgeDBAdapter } from "@auth/edgedb-adapter" import { createClient } from "edgedb" const client = createClient({ dsn: import.meta.env.AUTH_EDGEDB_DSN }) export const { onRequest, useSession, useSignIn, useSignOut } = QwikAuth$( () => ({ providers: [], adapter: EdgeDBAdapter(client), }) )注意 Qwik 端使用import.meta.env.AUTH_EDGEDB_DSN读取环境变量,这是 Vite 系构建工具的标准写法。
4.3 SvelteKit
import { SvelteKitAuth } from "@auth/sveltekit" import { EdgeDBAdapter } from "@auth/edgedb-adapter" import { createClient } from "edgedb" const client = createClient({ dsn: process.env.AUTH_EDGEDB_DSN }) export const { handle, signIn, signOut } = SvelteKitAuth({ adapter: EdgeDBAdapter(client), providers: [], })4.4 Express
import { ExpressAuth } from "@auth/express" import { EdgeDBAdapter } from "@auth/edgedb-adapter" import { createClient } from "edgedb" const app = express() const client = createClient({ dsn: process.env.AUTH_EDGEDB_DSN }) app.set("trust proxy", true) app.use( "/auth/*", ExpressAuth({ providers: [], adapter: EdgeDBAdapter(client), }) )所有框架的接入模式完全一致:createClient({ dsn })创建客户端 →EdgeDBAdapter(client)包装成适配器 → 传给框架对应的 Auth 初始化函数。其余部分(provider 配置、回调等)与使用其他数据库适配器时的写法相同。
五、适配器源码实现深度解析
适配器的完整实现集中在 packages/adapter-edgedb/src/index.ts,所有方法均使用 EdgeDB 的原生 EdgeQL 通过client.querySingle/client.queryRequiredSingle/client.execute执行。以下按Adapter接口的分组逐一拆解。
5.1 用户管理(User)
创建用户createUser(源码 L29-L59):
async createUser({ email, emailVerified, name, image }) { return await client.queryRequiredSingle( ` with image := <optional str>$image, name := <optional str>$name, emailVerified := <optional str>$emailVerified select ( insert User { email:= <str>$email, emailVerified:= <datetime>emailVerified, name:= name, image:= image, } ) { id, email, emailVerified, name, image } `, { email, emailVerified: emailVerified && new Date(emailVerified).toISOString(), name, image, } ) }实现要点:
- 所有可选字段(
image、name、emailVerified)在 EdgeQL 中声明为<optional str>,避免传入undefined时类型报错; - JS 侧的
Date在传入前被转换为 ISO 字符串,EdgeQL 侧再用<datetime>显式转型为数据库的datetime类型; - 使用
insert ... select结构,插入后立即以投影形式返回完整用户对象。
查询用户:getUser(id)、getUserByEmail(email)分别按User.id(<uuid>$id)和User.email(<str>$email)过滤,返回AdapterUser或null(见 源码 L60-L87)。
按账号查用户getUserByAccount(源码 L88-L106)是数据库适配器实现的关键技巧:先用子查询with account := (select Account filter ...)找到账号,再通过反向链接account.user取出关联的用户,一条 EdgeQL 完成关联查询,无需两次往返。
更新用户updateUser(源码 L107-L141)使用 EdgeQL 的合并运算符??实现「传入才更新,未传保留原值」的语义:
set { email := email ?? .email, emailVerified := <datetime>emailVerified ?? .emailVerified, image := image ?? .image, name := name ?? .name, }删除用户deleteUser(源码 L142-L144)直接执行delete User filter .id = <uuid>$id;。由于 ESDL schema 中Account、Session都声明了on target delete delete source,删除 User 时关联的账号与会话会被级联删除,这是数据一致性由数据库层保证的典型设计。
5.2 账号关联(Account)
linkAccount(源码 L145-L200)插入一条Account记录,并把user链接指向按userId查到的User:
insert Account { type := <str>$type, provider := <str>$provider, providerAccountId := <str>$providerAccountId, ... user := ( select User filter .id = <uuid>userId ) }这里有一个值得注意的细节:expires_at在 OAuth 返回中通常是秒级数字,适配器会先String(expires_at)转为字符串,再在 EdgeQL 中<int64>转回整数(源码 L193)。unlinkAccount(源码 L201-L211)则按providerAccountId + provider组合条件删除对应 Account。
5.3 会话管理(Session)
createSession(源码 L212-L231)插入Session并返回{ expires, sessionToken, userId }。
getSessionAndUser(源码 L232-L268)是数据库会话策略的核心:一次查询同时拉取会话和其关联用户(EdgeDB 的链接投影天然支持嵌套),返回{ user, session };若任一部分缺失则返回null。这与@auth/core中「支持联表查询的数据库应减少往返次数」的建议完全吻合。
updateSession(源码 L269-L300)同样用??保留旧值,并且对用户链接使用了assert_exists(user ?? .user)保证不会产生悬空引用。deleteSession按sessionToken删除。
5.4 验证令牌(VerificationToken)
createVerificationToken(源码 L307-L327)插入令牌记录;useVerificationToken(源码 L328-L348)则采用「查询即删除」的原子语义——用delete ... filter .token = ... and .identifier = ...一次性取回并消费令牌,天然保证令牌只能使用一次,这正是@auth/core对useVerificationToken的契约要求。
5.5 适配器的自动化测试
packages/adapter-edgedb/test/index.test.ts 通过runBasicTests(来自 packages/utils/adapter.ts)对适配器跑一套跨数据库的统一基础测试:
const client = createClient() runBasicTests({ adapter: EdgeDBAdapter(client), db: { connect: async () => { /* 清空 User/Account/Session/VerificationToken 四张表 */ }, disconnect: async () => { /* 同上清理 */ }, user: async (id) => client.querySingle(`select User {...} filter .id = <uuid>$id`), account: async ({ providerAccountId, provider }) => { /* ... */ }, session: async (sessionToken) => { /* ... */ }, verificationToken: async ({ token, identifier }) => { /* ... */ }, }, })这意味着:只要你的 EdgeDB 实例可用,直接运行pnpm test(对应 vitest 配置)即可验证适配器的用户增删改查、账号链接、会话与令牌全流程是否符合 Auth.js 契约。
六、EdgeDB CLI 安装与项目初始化
6.1 安装 CLI
Linux / macOS:
curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | shWindows(PowerShell):
iwr https://ps1.edgedb.com -useb | iex安装后用edgedb --version验证。如果提示Command not found,通常需要重新打开一个终端窗口让 PATH 生效。
6.2 初始化项目
在应用根目录执行:
edgedb project init该命令会启动一个本地 EdgeDB 实例,并把当前目录与实例「链接」。此后只要你在该目录下运行 CLI 命令或使用客户端库,都能自动发现并连接这个实例,无需额外配置连接参数——这正是上文「本地开发可不传 DSN」的原因。
七、数据模型:替换 default.esdl
初始化后,用以下内容替换自动生成的dbschema/default.esdl。这是整个适配器能够正常工作的基石——它定义了 Auth.js 所需的四类模型:
module default { type User { property name -> str; required property email -> str { constraint exclusive; } property emailVerified -> datetime; property image -> str; multi link accounts := .<user[is Account]; multi link sessions := .<user[is Session]; property createdAt -> datetime { default := datetime_current(); }; } type Account { required property userId := .user.id; required property type -> str; required property provider -> str; required property providerAccountId -> str { constraint exclusive; }; property refresh_token -> str; property access_token -> str; property expires_at -> int64; property token_type -> str; property scope -> str; property id_token -> str; property session_state -> str; required link user -> User { on target delete delete source; }; property createdAt -> datetime { default := datetime_current(); }; constraint exclusive on ((.provider, .providerAccountId)) } type Session { required property sessionToken -> str { constraint exclusive; } required property userId := .user.id; required property expires -> datetime; required link user -> User { on target delete delete source; }; property createdAt -> datetime { default := datetime_current(); }; } type VerificationToken { required property identifier -> str; required property token -> str { constraint exclusive; } required property expires -> datetime; property createdAt -> datetime { default := datetime_current(); }; constraint exclusive on ((.identifier, .token)) } } # Disable the application of access policies within access policies # themselves. This behavior will become the default in EdgeDB 3.0. # See: https://www.edgedb.com/docs/reference/ddl/access_policies#nonrecursive using future nonrecursive_access_policies;几个需要重点理解的设计:
- 反向链接与计算属性:
User.accounts/User.sessions使用.<user[is Account]反向链接语法定义;Account.userId、Session.userId是通过.user.id计算的属性,查询时无需额外存储,与@auth/core中AdapterSession.userId字段一一对应。 - 级联删除:
Account与Session的user链接都声明了on target delete delete source,删除用户时其账号与会话自动清除(对应源码deleteUser的级联行为)。 - 唯一性约束:
User.email、Account.providerAccountId、Session.sessionToken、VerificationToken.token各自exclusive;同时用constraint exclusive on ((.provider, .providerAccountId))和constraint exclusive on ((.identifier, .token))定义了复合唯一约束,防止同一 OAuth 账号或同一验证令牌被重复绑定/使用。 - 时间字段:所有模型补充了
createdAt -> datetime { default := datetime_current() },作为审计字段;emailVerified、expires使用 EdgeDB 的datetime类型,与源码中<datetime>转型对应。 - 末尾的
using future nonrecursive_access_policies;是 EdgeDB 2.x 兼容 3.0 默认行为的声明,用于避免访问策略内部递归应用,保留该行即可。
模型命名与字段与@auth/core的Adapter契约(packages/core/src/adapters.ts 中AdapterUser/AdapterAccount/AdapterSession/VerificationToken)严格对齐,因此适配器无需额外的字段映射逻辑。
八、迁移:创建并应用
- 生成迁移文件:
edgedb migration create该命令对比 schema 与数据库当前状态,生成迁移脚本。
- 应用迁移:
edgedb migrate建议在每次修改default.esdl后重复这两步,保持数据库与 schema 同步。
九、生成类型安全的查询构建器
为了让应用代码以「代码优先」的方式编写完全类型化的 EdgeQL,需要生成查询构建器:
npx @edgedb/generate edgeql-js生成后即可写出带完整类型推导的查询:
const query = e.select(e.User, () => ({ id: true, email: true, emailVerified: true, name: true, image: true, filter_single: { email: "johndoe@example.com" }, })) return await query.run(client)注意:适配器源码本身使用原生 EdgeQL 字符串(queryRequiredSingle等),并不依赖生成的查询构建器;查询构建器是给应用业务代码使用的类型安全工具,两者互不冲突。从 packages/adapter-edgedb/tsconfig.json 可以看出,适配器包仅编译src目录,构建产物为 ESM("type": "module")。
十、部署到生产环境
10.1 部署 EdgeDB 实例
先在云厂商部署一个 EdgeDB 实例,官方支持的部署途径包括 AWS、Google Cloud、Azure、DigitalOcean、Fly.io 以及 Docker(云厂商无关方案)。
10.2 获取 DSN
DSN 即连接字符串,格式为edgedb://username:password@hostname:port,具体获取方式取决于云厂商的控制台/CLI。
10.3 设置环境变量
在.env中写入:
AUTH_EDGEDB_DSN=edgedb://johndoe:supersecure@myhost.com:420确保应用运行环境(如 Vercel、Render、自有服务器)也注入该变量。
10.4 对远程实例应用迁移
用 DSN 指向远程实例执行迁移:
edgedb migrate --dsn <your-instance-dsn>10.5 配置 prebuild 脚本
在package.json中添加prebuild钩子:宿主平台构建初始化时会先触发它,读取EDGEDB_DSN环境变量连接数据库并生成查询构建器,再开始编译项目:
"scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", + "prebuild": "npx @edgedb/generate edgeql-js" },注意prebuild读取的是EDGEDB_DSN(EdgeDB 工具的默认变量名),而应用运行时代码读取的是AUTH_EDGEDB_DSN,两者在部署平台上都应配置为同一个 DSN 值。
十一、小结
@auth/edgedb-adapter是一个结构清晰、契约完整的官方数据库适配器:
- 接入简单:
createClient({ dsn })+EdgeDBAdapter(client)两行代码即可接入 Next.js、Qwik、SvelteKit、Express 等任意支持 Auth.js 的框架; - 实现完整:源码 覆盖
Adapter接口的用户、账号、会话、验证令牌全部方法,并在单条 EdgeQL 内完成关联查询、级联删除、原子消费令牌等操作; - 可测试:测试用例 通过统一的
runBasicTests套件保证与 Auth.js 核心契约的兼容性; - 配套完善:仓库内的 配套文档 提供了从 CLI 安装、schema 定义到云端部署的端到端指引,配合本文的源码级解析,足以支撑你在生产项目中稳定落地 EdgeDB 认证存储方案。
【免费下载链接】next-authAuthentication for the Web.项目地址: https://gitcode.com/gh_mirrors/ne/next-auth
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考