Nhost 后端示例深度解析:基于 tasks / attachments 数据模型的权限与函数设计
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
本文以仓库中的 examples/guides/backend/README.md 为骨架,逐层拆解一个"小而完整"的 Nhost 后端示例:它用
tasks与attachments两张表演示了典型的按所有者(ownership)隔离的权限模型,并附带一个echo函数演示 Serverless Functions 的基本形态。读完本文,你将掌握 Nhost 中表结构迁移(Migrations)、Hasura 元数据权限(Permissions)、存储(Storage)文件归属,以及函数(Functions)从定义到运行验证的完整实战链路,并能直接对照仓库源码验证每个环节的实现细节。
示例后端的设计定位
该示例位于 examples/guides/backend 目录,其 README 明确说明它是一个"非常简单"的 Nhost 后端,用途是演示如何配合各种正在试验中的 SDK 使用 Nhost。也就是说,它是整个examples/guides系列(如 react-query 指南、react-apollo、react-urql、codegen-nhost 等)共用的后端底座,前端指南中的查询、变更、订阅全部围绕它展开。
整个示例后端的组成非常清晰,只有三大部分:
- 数据库 Schema:
tasks表 +attachments表; - 权限(Permissions):面向
user角色的按归属隔离读写规则; - 函数(Functions):一个返回请求信息的
echo函数。
目录结构在仓库中完整可查:
examples/guides/backend/ ├── README.md # 本文档(示例说明) ├── Makefile # dev-env-up / dev-env-down 快捷命令 ├── env-up.sh # 启动脚本(nhost up) ├── functions/ # 函数目录(package.json / tsconfig.json) └── nhost/ ├── config.yaml # Nhost 配置(version: 3) ├── nhost.toml # 完整配置:hasura / auth / storage / postgres / functions ├── metadata/ # Hasura 元数据(权限、关系、自定义字段) └── migrations/ # 数据库迁移(SQL)数据库 Schema:两张表构建"任务 + 附件"模型
原文档用两个清单定义了数据模型,以下逐一对照仓库中的迁移 SQL 进行还原。
tasks 表
原文档定义的tasks表包含 7 个字段:
id(UUID)created_at(Timestamp)updated_at(Timestamp)user_id(外键指向auth.users.id)title(Text)description(Text)completed(Boolean)
对应的真实建表语句位于 migrations/default/1738758216166_create_table_public_tasks/up.sql:
CREATE TABLE "public"."tasks" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "title" text NOT NULL, "description" text NOT NULL, "completed" boolean NOT NULL DEFAULT false, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON UPDATE cascade ON DELETE cascade );这段 SQL 有 4 个值得注意的实现细节:
id默认gen_random_uuid():主键由 PostgreSQL 自动生成 UUID,无需应用层干预;该函数依赖pgcrypto扩展,因此迁移文件末尾有CREATE EXTENSION IF NOT EXISTS pgcrypto;。- 时间戳默认值:
created_at与updated_at都默认为now(),保证插入即有时间。 - 外键指向 auth schema:
user_id引用的是 Nhost Auth 服务管理的auth.users表,这是"登录用户"与"业务数据"之间建立归属关系的标准做法。 updated_at自动刷新:迁移中附带了一个触发器set_public_tasks_updated_at,配合函数set_current_timestamp_updated_at(),在每次UPDATE时自动把updated_at刷为当前时间——这也是文档中updated_at(Timestamp)字段"可写但通常不用手写"的原因。
attachments 表
原文档定义的attachments表是典型的多对多关联表:
task_id(外键指向tasks.id)file_id(外键指向storage.files.id)
对应的建表语句位于 migrations/default/1738758597255_create_table_public_attachments/up.sql:
CREATE TABLE "public"."attachments" ( "task_id" uuid NOT NULL, "file_id" uuid NOT NULL, PRIMARY KEY ("task_id","file_id"), FOREIGN KEY ("task_id") REFERENCES "public"."tasks"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("file_id") REFERENCES "storage"."files"("id") ON UPDATE restrict ON DELETE restrict );从源码可以看到几点超出原文档的描述:
- 联合主键:
(task_id, file_id)构成复合主键,天然防止同一文件被重复挂到同一任务上。 - 关联的是 Nhost 的存储服务表:
storage.files由 Nhost Storage 服务管理(对应 nhost.toml 中的[storage] version = '0.7.2'),该表本身就带uploaded_by_user_id等归属字段,见 storage_files.yaml。 - 删除策略有区分:
task_id是ON DELETE CASCADE(任务删了附件记录跟着删),file_id是ON DELETE RESTRICT(文件被引用时不允许删除),避免悬空引用。
元数据层的"自定义命名"
Hasura 元数据 public_tasks.yaml 与 public_attachments.yaml 中,对列做了 GraphQL 命名映射:
tasks:created_at → createdAt、updated_at → updatedAt、user_id → userID;attachments:task_id → taskID,并为file_id、task_id声明了两个对象关系file和task。
也就是说,数据库列名保持 snake_case,而 GraphQL API 对外暴露 camelCase 字段,前端开发者写查询时用createdAt、userID、taskID,这是 Nhost(基于 Hasura)元数据配置的典型玩法。
权限模型:让用户只能操作自己的数据
原文档给出了三段权限描述,这是本示例的核心知识点,也是文档中最具实战价值的部分:
tasks:user角色可以 insert/select/update 自己拥有的任务,归属由user_id列追踪,且插入时从会话自动设置该列;attachments:user角色可以 insert/select/delete 自己拥有的任务与文件的附件;storage.files:user角色可以 insert/select/delete 自己拥有的文件。
下面分别对照元数据文件还原其在 Hasura 中的真实写法。
tasks:插入时自动写入 user_id
public_tasks.yaml 中insert_permissions的定义如下:
insert_permissions: - role: user permission: check: user_id: _eq: X-Hasura-User-Id set: user_id: x-hasura-User-Id columns: - completed - description - title关键机制是set段:用户插入时无需也不能自己指定user_id,Hasura 会从会话变量x-hasura-User-Id自动填充——这正是原文档所说"Ownership is tracked by theuser_idcolumn which is set automatically on insert from the session"。而check又确保即使有人绕过前端提交user_id,也会被校验拒绝(必须等于会话中的用户 ID)。columns限制了用户可写的列只有title/description/completed。
其余三类权限同样围绕归属过滤:
select_permissions: - role: user permission: columns: [completed, description, title, created_at, updated_at, id, user_id] filter: user_id: { _eq: X-Hasura-User-Id } update_permissions: - role: user permission: columns: [completed, description, title] filter: {} # 先按此条件定位可更新的行 check: user_id: { _eq: X-Hasura-User-Id } # 更新后仍必须属于自己的行 delete_permissions: - role: user permission: filter: user_id: { _eq: X-Hasura-User-Id }实现要点:select/delete通过filter把查询范围锁定在当前用户;update额外有check,保证更新后行依然归属当前用户(防止把任务"改给"别人)。
attachments:跨表归属校验
public_attachments.yaml 的权限比tasks复杂——它要同时校验"任务是我的"且"文件是我的"。实现方式是借助前面声明的对象关系file与task,做嵌套过滤:
insert_permissions: - role: user permission: check: _and: - file: { uploaded_by_user_id: { _eq: X-Hasura-User-Id } } - task: { user_id: { _eq: X-Hasura-User-Id } } columns: [file_id, task_id]select与delete使用同样的_and组合过滤:
select_permissions: - role: user permission: columns: [file_id, task_id] filter: _and: - file: { uploaded_by_user_id: { _eq: X-Hasura-User-Id } } - task: { user_id: { _eq: X-Hasura-User-Id } } delete_permissions: - role: user permission: filter: _and: - file: { uploaded_by_user_id: { _eq: X-Hasura-User-Id } } - task: { user_id: { _eq: X-Hasura-User-Id } }这意味着:用户只能为"自己的任务"挂"自己上传的文件",以及只能读取/删除这样的关联记录——原文档中"insert/select/delete attachments for tasks and files that they own"的精确含义在此落地。
storage.files:文件归属的存储侧控制
storage_files.yaml 对 Nhost Storage 的files表做了同构控制:插入时通过set: { uploaded_by_user_id: x-hasura-User-Id }自动记录上传者,select/update/delete全部以uploaded_by_user_id: { _eq: X-Hasura-User-Id }过滤,从而形成"我的文件只能我读写删"的闭环。
函数:echo —— 最简单的 Serverless Function
原文档对函数的描述只有一句:"一个名为echo的simple函数,会原样返回一些请求信息"。虽然examples/guides/backend/functions/目录下目前只有脚手架文件(package.json),但同仓库的 examples/demos/backend/functions/echo.ts 给出了该函数的标准实现,可以直接作为参考:
import process from "node:process"; import type { Request, Response } from "express"; import cors from "cors"; const corsMiddleware = cors(); export default (req: Request, res: Response) => { corsMiddleware(req, res, () => { res.status(200).json({ headers: req.headers, query: req.query, body: req.body, method: req.method, node: process.version, arch: process.arch, invocationId: req.invocationId, }); }); };这个实现完整呼应了"return back some request information"的语义:它把headers、query、body、method以及 Node 运行时版本、架构、调用 ID(invocationId)打包成 JSON 返回。同时可以看到 Nhost Functions 的两个约定:
- 默认导出:每个函数文件通过
export default导出一个 Express 风格的处理函数(req, res) => void; - CORS 中间件:示例统一包裹
cors()中间件,便于浏览器端直接调用。
函数运行时由 nhost.toml 的[functions.node] version = 22指定(Node.js 22),这是本地开发环境实际使用的运行时版本。
从零跑起这个后端
仓库提供了两条启动路径,均在examples/guides/backend目录下:
方式一:Makefile 快捷命令(推荐)
make dev-env-up # 等价于执行 ./env-up.sh make dev-env-down # 等价于 nhost down --volumes方式二:直接执行脚本
env-up.sh 的内容如下:
#!/bin/sh # if .secrets file doesn't exist, cp .secrets.example .secrets if [ ! -f .secrets ]; then cp .secrets.example .secrets fi nhost up脚本做了两件事:若本地缺少.secrets文件则从.secrets.example复制(nhost up会读取其中的密钥占位符),然后执行nhost up拉起完整本地环境。停止时使用nhost down --volumes可连数据卷一并清理,保证可重复重建。
启动后,Nhost CLI 会根据 nhost.toml 编排全部服务,该文件中的关键配置包括:
- Hasura:
version = 'v2.46.0-ce',开发模式开启(devMode = true)、Console 可用(enableConsole = true),JWT 密钥与 Admin Secret 通过{{ secrets.XXX }}占位符注入; - Auth:
version = '0.41.1',默认角色user([auth.user.roles] default = 'user'),允许user与me两种角色;邮箱密码登录启用、emailVerificationRequired = false、密码最短 9 位,并有邮箱/短信/暴力破解/注册/全局五档限流配置; - Storage:
version = '0.7.2'; - Postgres:
version = '14.20-20251217-1',存储容量 1GB; - 重定向白名单:
allowedUrls含http://localhost:5173与exp://192.168.1.103:8081,兼顾 Web 与 React Native 开发。
与其他 SDK 指南的衔接
这个后端示例的价值在于"被复用"。examples/guides系列的其他指南都直接基于它演示前端集成,最典型的是 react-query/README.md:
- 它演示了用
@nhost/nhost-js创建客户端(createClient({ region: "local", subdomain: "local" }))并注入会话; - 通过
useAuthenticatedFetcher封装带鉴权的 GraphQL 请求; - 使用 GraphQL CodeGen 生成类型安全的 React Query hooks,在组件中调用
useGetNinjaTurtlesWithCommentsQuery/useAddCommentMutation。
也就是说,tasks/attachments是"后端能力底座",而 ninja_turtles / comments 等是演示用扩展表。需要说明的是,后端目录的迁移中还包含几张原文档未提及的扩展表,如 ninja_turtles、comments(其权限同样遵循"user_id自动填充、只能改删自己的评论"的模式,见 public_comments.yaml)以及带种子数据的 movies 表。它们不影响本文所述核心模型,仅作为不同指南演示时的附加素材。
小结
通过这个示例后端,可以完整看到 Nhost "开箱即用的后端" 的三层结构:
| 层次 | 载体 | 本示例中的体现 |
|---|---|---|
| 数据层 | migrations/*.sql | tasks、attachments及扩展表 |
| 权限层 | metadata/.../tables/*.yaml | 基于X-Hasura-User-Id的按归属过滤与自动填充 |
| 计算层 | functions/ | echo请求信息回显函数 |
原文档"非常简单的后端"背后,隐藏着 Nhost 生产级权限体系的标准范式:外键指向auth.users建立归属、Hasura 元数据用会话变量约束读写、Storage 文件同样纳入归属模型、函数以 Express 风格默认导出。对照 迁移 SQL、元数据 与 echo 函数实现,即可把这套模式完整迁移到自己的项目中。
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考