Wasp 的 Automatic CRUD:用一行声明自动生成 React + Node.js 增删改查后端
2026/9/16 15:58:10 网站建设 项目流程

Wasp 的 Automatic CRUD:用一行声明自动生成 React + Node.js 增删改查后端

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

Automatic CRUD 是 Wasp 框架提供的高层抽象:你只需在 Wasp 声明文件中用一段简短的crud声明,就能让 Wasp 自动生成针对某个 Prisma 实体的增删改查 Queries 与 Actions,并在实体定义变化时自动重新生成后端逻辑。本文以 Wasp 仓库(waspc 生成器、代码生成模板)为依据,从概念、完整实战示例到 API 参考,带你掌握声明式后端开发的这套核心能力。

为什么需要 Automatic CRUD

如果你写过不少全栈应用,大概率会发现自己反复在做同一类重复工作:列出数据、添加数据、编辑数据、删除数据。这些"无聊的部分"正是 Automatic CRUD 要消除的对象。

在 Wasp 中,只需要一条声明,就可以告诉框架为某个实体自动生成创建(create)、读取(get / getAll)、更新(update)、删除(delete)所对应的服务端逻辑——也就是 Wasp 的 Queries 与 Actions。当你更新实体的定义时,Wasp 会自动重新生成对应的后端代码,实体与接口始终同步。

从源码结构看,这一能力在 Wasp 编译器中是一条完整的生成流水线:

  • waspc/src/Wasp/AppSpec/Crud.hs 定义了crud声明的内部数据结构(CrudCrudOperationsCrudOperationOptionsCrudOperation);
  • waspc/src/Wasp/Generator/Crud.hs 负责把声明转换成生成器所需的 JSON 配置(操作名、路由、是否公开、实体名等);
  • waspc/src/Wasp/Generator/Crud/Routes.hs 负责生成路由路径;
  • waspc/data/Generator/templates/server/src/crud/_operations.ts 等模板文件最终渲染出可运行的 Node.js 实现与客户端 SDK。

注意:该功能目前处于早期预览阶段(Early preview),Wasp 团队仍在积极迭代中,具体规划见文末 CRUD 的未来。

核心概念:一条 crud 声明做了什么

假设我们有如下Task实体(schema.prisma 中同样定义了任务类实体,便于对照):

model Task { id Int @id @default(autoincrement()) description String isDone Boolean }

接着在main.wasp中定义名为Taskscrud。我们指定使用Task实体,并启用getAllgetcreateupdate四个操作(假设不需要delete):

crud Tasks { entity: Task, operations: { getAll: { isPublic: true, // by default only logged in users can perform operations }, get: {}, create: { overrideFn: import { createTask } from "@src/tasks", }, update: {}, }, }

这段声明做了三件事:

  1. getAllgetupdate使用默认实现;
  2. create指定了自定义实现(overrideFn),指向@src/tasks.{js,ts}中导出的createTask函数;
  3. getAll被标记为公开(isPublic: true,无需登录即可访问),其余操作默认私有(仅登录用户可调用)。

Tasks crud 声明的可视化示意(图片来源:web/static/img/crud_diagram.png)

声明完成后,你就可以在客户端代码中直接使用生成的 Queries 和 Actions 了。接下来用一个完整的 TODO 应用把整个流程跑通。

实战示例:构建一个带登录的 TODO 应用

下面创建一个完整的使用 Automatic CRUD 的应用。我们沿用上一节的Task实体,再新增一个User实体,并开启基于用户名和密码的认证。

基于用户名认证的简易任务应用演示(图片来源:web/static/img/crud-guide.gif)

1. 创建应用

运行wasp new tasksCrudApp初始化项目,然后把以下内容写入main.wasp

app tasksCrudApp { wasp: { version: "{latestWaspVersion}" }, title: "Tasks Crud App", // We enabled auth and set the auth method to username and password auth: { userEntity: User, methods: { usernameAndPassword: {}, }, onAuthFailedRedirectTo: "/login", }, } // Tasks app routes route RootRoute { path: "/", to: MainPage } page MainPage { component: import { MainPage } from "@src/MainPage", authRequired: true, } route LoginRoute { path: "/login", to: LoginPage } page LoginPage { component: import { LoginPage } from "@src/LoginPage", } route SignupRoute { path: "/signup", to: SignupPage } page SignupPage { component: import { SignupPage } from "@src/SignupPage", }

关键点说明:

  • app块配置了应用名称与auth;这里将userEntity指向User,认证方法为usernameAndPassword,未登录访问受保护页时重定向到/login
  • MainPage设置了authRequired: true,未登录用户会被拦下;
  • 路由//login/signup分别指向三个页面组件。

然后在schema.prisma中定义实体:

model User { id Int @id @default(autoincrement()) tasks Task[] } // We defined a Task entity on which we'll enable CRUD later on model Task { id Int @id @default(autoincrement()) description String isDone Boolean userId Int user User @relation(fields: [userId], references: [id]) }

运行wasp db migrate-dev创建数据库并执行迁移。

2. 为 Task 实体添加 CRUD

main.wasp中加入如下crud声明:

// ... crud Tasks { entity: Task, operations: { getAll: {}, create: { overrideFn: import { createTask } from "@src/tasks", }, }, }

注意这里只启用了getAllcreate,意味着只会生成这两个操作。同时我们用overrideFn覆盖了create的默认实现:create操作将不再由框架生成,而是改用@src/tasks.{js,ts}中的createTask函数。

3. 编写自定义 create 操作

为什么要自定义create?因为我们希望新建的任务能关联到"创建它的那个用户"。Automatic CRUD 目前默认不知道这种业务关联(详见 CRUD 的未来 与 默认实现),所以需要覆盖。

src/tasks.js(JavaScript 版本):

import { HttpError } from 'wasp/server' export const createTask = async (args, context) => { if (!context.user) { throw new HttpError(401, 'User not authenticated.') } const { description, isDone } = args const { Task } = context.entities return await Task.create({ data: { description, isDone, // Connect the task to the user that is creating it user: { connect: { id: context.user.id, }, }, }, }) }

src/tasks.ts(TypeScript 版本):

import { type Tasks } from 'wasp/server/crud' import { type Task } from 'wasp/entities' import { HttpError } from 'wasp/server' type CreateTaskInput = { description: string; isDone: boolean } export const createTask: Tasks.CreateAction<CreateTaskInput, Task> = async ( args, context ) => { if (!context.user) { throw new HttpError(401, 'User not authenticated.') } const { description, isDone } = args const { Task } = context.entities return await Task.create({ data: { description, isDone, // Connect the task to the user that is creating it user: { connect: { id: context.user.id, }, }, }, }) }

关于 TypeScript 类型的补充说明(这也是理解整个覆盖机制的关键):

  • Wasp 会根据main.wasp中的 CRUD 声明自动生成Tasks.CreateAction类型,用它来标注覆盖函数的实现;
  • Tasks.CreateAction与 Wasp 为 Queries 和 Actions 生成类型的机制完全一致:标注后 TypeScript 能推断context对象的类型,而两个泛型参数分别指定 Action 的输入与输出;
  • 更完整的类型支持说明见 API 参考中的 Defining the overrides。

4. 在客户端使用生成的 CRUD 操作

src/MainPage.jsx(JavaScript 版本)中:

import { Tasks } from 'wasp/client/crud' import { useState } from 'react' export const MainPage = () => { const { data: tasks, isLoading, error } = Tasks.getAll.useQuery() const createTask = Tasks.create.useAction() const [taskDescription, setTaskDescription] = useState('') function handleCreateTask() { createTask({ description: taskDescription, isDone: false }) setTaskDescription('') } if (isLoading) return <div>Loading...</div> if (error) return <div>Error: {error.message}</div> return ( <div style={{ fontSize: '1.5rem', display: 'grid', placeContent: 'center', height: '100vh', }} > <div> <input value={taskDescription} onChange={(e) => setTaskDescription(e.target.value)} /> <button onClick={handleCreateTask}>Create task</button> </div> <ul> {tasks.map((task) => ( <li key={task.id}>{task.description}</li> ))} </ul> </div> ) }

src/MainPage.tsx(TypeScript 版本,得益于全栈类型安全,所有载荷类型自动推断):

import { Tasks } from 'wasp/client/crud' import { useState } from 'react' export const MainPage = () => { // Thanks to full-stack type safety, all payload types are inferred // automatically const { data: tasks, isLoading, error } = Tasks.getAll.useQuery() const createTask = Tasks.create.useAction() const [taskDescription, setTaskDescription] = useState('') function handleCreateTask() { createTask({ description: taskDescription, isDone: false }) setTaskDescription('') } if (isLoading) return <div>Loading...</div> if (error) return <div>Error: {error.message}</div> return ( <div style={{ fontSize: '1.5rem', display: 'grid', placeContent: 'center', height: '100vh', }} > <div> <input value={taskDescription} onChange={(e) => setTaskDescription(e.target.value)} /> <button onClick={handleCreateTask}>Create task</button> </div> <ul> {tasks.map((task) => ( <li key={task.id}>{task.description}</li> ))} </ul> </div> ) }

这里有两个值得注意的 API:

  • Tasks.getAll.useQuery():返回{ data, isLoading, error },是 React Query 风格的 hooks,与 Wasp 普通 Query 在客户端的使用方式一致;
  • Tasks.create.useAction():返回一个可直接调用的 Action 函数,调用后执行后端create

登录与注册页面使用 Wasp 的 Auth UI 组件即可:

src/LoginPage.jsx/src/LoginPage.tsx

import { LoginForm } from 'wasp/client/auth' import { Link } from 'react-router-dom' export function LoginPage() { return ( <div style={{ display: 'grid', placeContent: 'center', }} > <LoginForm /> <div> <Link to="/signup">Create an account</Link> </div> </div> ) }

src/SignupPage.jsx/src/SignupPage.tsx

import { SignupForm } from 'wasp/client/auth' export function SignupPage() { return ( <div style={{ display: 'grid', placeContent: 'center', }} > <SignupForm /> </div> ) }

到这里就完成了。运行wasp start即可看到应用:先出现登录/注册页,登录后会看到任务列表和新建任务的表单。

从源码看 CRUD 的生成机制

了解声明如何被编译成实际代码,有助于你在排查问题和扩展功能时心中有数。以当前仓库的 waspc 实现为例,整条链路如下:

1. 声明的内部表示

waspc/src/Wasp/AppSpec/Crud.hs 定义了Crud记录,包含entity :: Ref Entity(目标实体引用)和operations :: CrudOperationsCrudOperationsget/getAll/create/update/delete均为可选字段,每个操作对应CrudOperationOptions,其字段正是isPublic :: Maybe BooloverrideFn :: Maybe ExtImport——与文档中声明语法一一对应。

2. 生成 JSON 配置

waspc/src/Wasp/Generator/Crud.hs 的getCrudOperationJson把每个启用的操作转换成生成器模板所需的 JSON,其中"isPublic" .= fromMaybe False (AS.Crud.isPublic options)明确说明:未指定时isPublic默认值为False。同时它还会带上实体的entityUpper(大写实体名)与entityLower(小写实体名)用于模板插值。

3. 路由生成

waspc/src/Wasp/Generator/Crud/Routes.hs 定义了每个操作的 HTTP 路由片段:Get -> "get"GetAll -> "get-all"Create -> "create"Update -> "update"Delete -> "delete",完整路径为crud/{crud名}/{操作名}(例如crud/tasks/get-all)。这一点在 waspc/tests/Generator/CrudTest.hs 的单元测试中也有断言(例如"GetAll" .= mkOperationJson "get-all" "crud/tasks/get-all" NotPublic)。

4. Express 路由注册

waspc/data/Generator/templates/server/src/routes/crud/_crud.ts 为每个操作注册了 Express 路由处理器,全部使用POST请求,并通过createQuery/createAction中间件包装——这说明 CRUD 操作在底层就是 Wasp 的 Queries 和 Actions。若应用开启了认证,路由还会挂载auth中间件。

5. 默认实现的渲染

最核心的模板是 waspc/data/Generator/templates/server/src/crud/_operations.ts。它展示了默认实现与覆盖的完整逻辑:

  • 若某操作没有overrideFn,模板会生成默认实现;若操作不是公开的,先调用throwIfNotAuthenticated(context)做登录校验(开启认证时若context.user为空则抛出createInvalidCredentialsError());
  • 若操作配置了overrideFn,则直接用用户导入的importIdentifier引用替换默认实现;
  • 最终getAllFn/getFn/createFn/updateFn/deleteFn作为 Express 路由处理函数导出,并注入entities对象(const entities = { {= crud.entityUpper =}: prisma.{= crud.entityLower =} })。

6. 客户端 SDK 与类型

客户端 API 由 waspc/data/Generator/templates/sdk/wasp/client/crud/_crud.ts 生成:每个操作对外暴露query/useQuery(Query 类)或action/useAction(Action 类),底层调用createQuery/createAction。服务端类型(Tasks.GetAllQueryTasks.CreateAction等)则由 waspc/data/Generator/templates/sdk/wasp/server/crud/_operationTypes.ts 生成,其输入/输出类型与 Prisma 的WhereUniqueInputCreateInputUpdateInput类型对齐。

CRUD 的未来

当前 CRUD 操作对它所实现的业务逻辑了解非常有限:

  • 它不知道"任务应该关联到创建它的用户"这类业务规则——这正是上面示例中必须覆盖create操作的原因;
  • 它不了解授权规则,例如"用户不能为其他用户创建任务"。Wasp 未来将引入基于角色的授权(role-based authorization),并计划让 CRUD 操作感知授权规则;
  • 它没有输入校验与清洗能力,例如无法保证任务描述非空。

CRUD 操作是快速搭建后端的机制,但它能提供的开箱即用能力取决于它从 Wasp 应用中能获取多少信息——应用提供的信息越充分,CRUD 就越强大。Wasp 团队计划持续支持并发展 CRUD,让它成为创建后端最简单的方式(相关进展可关注 wasp-lang/wasp 仓库中的 issue #1253,文中不展开外部链接)。

API 参考

CRUD 声明建立在已有的实体声明之上。下面用两个示例完整探索其 API:一个依赖默认选项的基础声明,一个使用额外选项与覆盖的复杂声明。

声明 CRUD 并使用默认选项

为名为Task的实体创建 CRUD 操作,像这样写:

crud Tasks { // crud name here is "Tasks" entity: Task, operations: { get: {}, getAll: {}, create: {}, update: {}, delete: {}, }, }

Wasp 会提供如下默认实现(与 waspc/data/Generator/templates/server/src/crud/_operations.ts 中渲染的默认逻辑一致):

get—— 基于id字段返回单个实体(Wasp 使用 Prisma schema 中标记@id的字段作为 id 字段):

// ... // Wasp uses the field marked with `@id` in Prisma schema as the id field. return Task.findUnique({ where: { id: args.id } })

getAll—— 返回全部实体(若操作非公开,Wasp 会校验请求是否来自已认证用户):

// ... // If the operation is not public, Wasp checks if an authenticated user // is making the request. return Task.findMany()

create—— 创建新实体:

// ... return Task.create({ data: args.data })

update—— 更新已有实体:

// ... // Wasp uses the field marked with `@id` in Prisma schema as the id field. return Task.update({ where: { id: args.id }, data: args.data })

delete—— 删除已有实体:

// ... // Wasp uses the field marked with `@id` in Prisma schema as the id field. return Task.delete({ where: { id: args.id } })

TypeScript 项目中的声明与默认实现完全相同(模板生成的类型为RegisteredGetQuery/RegisteredGetAllQuery/RegisteredCreateAction/RegisteredUpdateAction/RegisteredDeleteAction)。

当前限制默认的createupdate实现会保存客户端发送的全部数据,这并不总是理想行为——例如客户端本不应能修改实体中的所有字段。未来 Wasp 计划为 Action 输入增加校验,只保存用户被允许修改的数据。目前的解决方案是提供覆盖函数:使用overrideFn选项替换默认实现,并自行编写校验逻辑。

声明 CRUD 并使用全部可用选项

一个更复杂的 CRUD 声明示例:

crud Tasks { // crud name here is "Tasks" entity: Task, operations: { getAll: { isPublic: true, // optional, defaults to false }, get: {}, create: { overrideFn: import { createTask } from "@src/tasks", // optional }, update: {}, }, }

CRUD 声明包含以下字段:

  • entity: Entity(必填) 要应用 CRUD 操作的实体。

  • operations: { [operationName]: CrudOperationOptions }(必填) 要生成的操作集合。键为操作名,值为操作配置。

    • operationName的合法取值:
      • getAll
      • get
      • create
      • update
      • delete
    • CrudOperationOptions可包含以下字段:
      • isPublic: bool—— 操作是否公开。公开则无需认证即可访问;非公开则仅限已认证用户。默认为false
      • overrideFn: ExtImport—— 可选覆盖实现的 Node.js 导入语句。

定义覆盖函数(Overrides)

与 Actions 和 Queries 类似,你可以在 JavaScript/TypeScript 文件中定义覆盖实现。覆盖函数接收两个参数:

  • args操作的参数,即客户端发送的数据。

  • context包含发起请求的user以及entities对象(其中包含被操作的实体)。

TypeScript 项目中,可以通过wasp/server/crud导入{crud 名}来获取每个可覆盖函数的类型,可用类型包括:

  • {crud name}.GetAllQuery
  • {crud name}.GetQuery
  • {crud name}.CreateAction
  • {crud name}.UpdateAction
  • {crud name}.DeleteAction

如果 CRUD 名为Tasks,导入方式如下:

import { type Tasks } from 'wasp/server/crud' // Each of the types is a generic type, so you can use it like this: export const getAllOverride: Tasks.GetAllQuery<Input, Output> = async ( args, context ) => { // ... }

每个类型都是泛型,两个类型参数分别对应输入与输出类型。使用示例见前文 为 Task 实体添加 CRUD 一节。

在客户端代码中使用 CRUD 操作

在客户端,从wasp/client/crud导入{crud 名}对象。例如 CRUD 名为Tasks

import { Tasks } from 'wasp/client/crud'

然后即可访问各个操作:

const { data } = Tasks.getAll.useQuery() const { data } = Tasks.get.useQuery({ id: 1 }) const createAction = Tasks.create.useAction() const updateAction = Tasks.update.useAction() const deleteAction = Tasks.delete.useAction()

注意getgetAll生成的是 Query(useQuery),createupdatedelete生成的是 Action(useAction)。

所有 CRUD 操作在底层都由 Queries 和 Actions 实现(这一点在 waspc/data/Generator/templates/server/src/routes/crud/_crud.ts 中通过createQuery/createAction中间件可以清晰看到),因此它们天然继承这些基础能力的所有特性,例如自动的 SuperJSON 序列化、TypeScript 下的全栈类型安全等。你也可以在仓库的 crud 生成器测试 中看到对公开操作、覆盖操作、路由路径等行为的自动化验证。

【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询