资讯动态

Wasp Automatic CRUD 完整实战指南:用一条 crud 声明生成全套增删改查后端

发布时间:2026/9/14 10:48:24 来源:尧图企业网站定制
Wasp Automatic CRUD 完整实战指南用一条 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在 Wasp 全栈框架中列表、新增、编辑、删除这类重复的后端样板代码几乎每个应用都要写一遍。Wasp 通过Automatic CRUD自动 CRUD提供了更高层的抽象只需在main.wasp中做一次声明即可自动生成针对某个实体Entity的 Queries 与 Actions即getAll、get、create、update、delete并在实体定义更新时自动重新生成后端逻辑。本文以 Wasp v0.14 官方文档为主体结合当前仓库中的 Haskell 生成器源码与 kitchen-sink 示例完整讲解 Automatic CRUD 的声明语法、默认实现、覆盖override机制、客户端调用方式与底层实现原理。说明本文基于 web/versioned_docs/version-0.14/data-model/crud.md 展开。该功能在 v0.14 中属于Early preview阶段官方仍在持续迭代中。Automatic CRUD 是什么如果你写过大量全栈应用一定重复做过这些事列出数据、新增数据、编辑数据、删除数据。Wasp 用Automatic CRUD这个概念让这些“无聊的部分”变得简单。核心思想是用一次声明告诉 Wasp 为某个实体自动生成服务端逻辑即 Queries 和 Actions用于创建、读取、更新和删除该实体。随着实体定义的更新Wasp 会自动重新生成对应的后端逻辑。最小示例为 Task 实体开启 CRUD假设我们有如下Task实体定义在schema.prismamodel Task { id Int id default(autoincrement()) description String isDone Boolean }接着在main.wasp中定义一个名为Tasks的crud指定使用Task实体并只启用getAll、get、create、update四个操作假设我们不需要deletecrud Tasks { entity: Task, operations: { getAll: { isPublic: true, // by default only logged in users can perform operations }, get: {}, create: { overrideFn: import { createTask } from src/tasks.js, }, update: {}, }, }这段声明包含三层含义getAll、get、update使用 Wasp 生成的默认实现create通过overrideFn指定了自定义实现指向src/tasks.js中导出的createTask函数getAll被标记为public无需认证即可访问其余操作默认private只有登录用户可访问。官方用下图直观展示这个声明图片来源web/static/img/crud_diagram.png声明完成后我们就可以在客户端代码中使用刚才指定的 CRUD Queries 和 Actions 了。实战示例一个带用户名密码认证的 TODO 应用下面我们构建一个完整应用来演示 Automatic CRUD 的实际用法。沿用上面的Task实体但额外增加User实体并启用基于 用户名和密码 的认证。第一步创建应用并配置基础文件先运行wasp new tasksCrudApp创建新应用然后在main.wasp中加入以下内容app tasksCrudApp { wasp: { version: ^0.14.0 }, 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.jsx, authRequired: true, } route LoginRoute { path: /login, to: LoginPage } page LoginPage { component: import { LoginPage } from src/LoginPage.jsx, } route SignupRoute { path: /signup, to: SignupPage } page SignupPage { component: import { SignupPage } from src/SignupPage.jsx, }再在schema.prisma中定义实体model User { id Int id default(autoincrement()) tasks Task[] } // We defined a Task entity on which well 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创建数据库并执行迁移。第二步为 Task 实体添加 CRUD在main.wasp中加入以下crud声明// ... crud Tasks { entity: Task, operations: { getAll: {}, create: { overrideFn: import { createTask } from src/tasks.js, }, }, }注意我们只启用了getAll和create两个操作这意味着只有这两个操作会生成、可用。同时create被overrideFn覆盖——生成器不会为它生成默认实现而是使用src/tasks.{js,ts}中的createTask函数。第三步编写自定义 create 操作为什么需要自定义create因为我们要保证新创建的任务一定归属于创建它的用户。而 Automatic CRUD 的默认实现还无法感知这种业务规则因此需要覆盖。src/tasks.{js,ts}的内容如下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, // 将任务关联到创建它的用户 user: { connect: { id: context.user.id, }, }, }, }) }TypeScript 版本利用 Wasp 生成的全栈类型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.CreateActionCreateTaskInput, 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, // 将任务关联到创建它的用户 user: { connect: { id: context.user.id, }, }, }, }) }Wasp 会根据main.wasp中的 CRUD 声明自动生成Tasks.CreateAction类型用来标注 CRUD Action 实现。该类型与 Wasp 为 Queries 和 Actions 生成的类型机制完全一致标注后 TypeScript 就能推断 Actioncontext对象的类型而两个类型参数分别用于指定 Action 的输入与输出类型。第四步在客户端使用生成的 CRUD 操作在客户端我们从wasp/client/crud导入Tasks对象然后像使用普通 Query/Action Hook 一样调用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 divLoading.../div if (error) return divError: {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 ) }TypeScript 版本几乎一致唯一的区别是得益于全栈类型安全Tasks.getAll.useQuery()与Tasks.create.useAction()的载荷类型会自动推断无需手动标注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 divLoading.../div if (error) return divError: {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 ) }第五步登录与注册页面登录和注册页面直接使用 Wasp 的 Auth UI 组件即可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/signupCreate an account/Link /div /div ) }import { SignupForm } from wasp/client/auth export function SignupPage() { return ( div style{{ display: grid, placeContent: center, }} SignupForm / /div ) }到这里应用就完成了。运行wasp start即可看到效果首先出现登录/注册页面登录成功后你会看到任务列表和创建新任务的表单。CRUD 的默认实现一条声明背后生成了什么当你不为某个操作提供overrideFn时Wasp 会为五种操作各生成一个默认实现。以下是针对名为Tasks、实体为Task的 CRUD启用全部五个操作时的默认行为crud Tasks { // crud name here is Tasks entity: Task, operations: { get: {}, getAll: {}, create: {}, update: {}, delete: {}, }, }对应的默认实现get— 根据id字段返回单个实体Wasp 使用 Prisma schema 中标记id的字段作为 id 字段// ... return Task.findUnique({ where: { id: args.id } })getAll— 返回所有实体如果该操作不是 publicWasp 会先校验请求是否来自已认证用户// ... // 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— 更新已有实体同样以id字段定位记录// ... return Task.update({ where: { id: args.id }, data: args.data })delete— 删除已有实体// ... return Task.delete({ where: { id: args.id } })默认实现的当前限制需要注意默认的create和update实现会原样保存客户端发送的所有数据。这并不总是我们想要的——例如客户端本不应能够修改实体中的全部字段。官方文档明确说明未来计划加入 action 输入校验只保存用户被允许修改的数据当前阶段的解决方案就是使用overrideFn提供覆盖实现自行实现校验逻辑详见下文“当前局限与未来规划”。API 参考CRUD 声明的完整字段CRUD 声明建立在已有的实体声明之上。下面用一个启用全部可用选项的复杂声明来完整展开 APIcrud Tasks { // crud name here is Tasks entity: Task, operations: { getAll: { isPublic: true, // optional, defaults to false }, get: {}, create: { overrideFn: import { createTask } from src/tasks.js, // 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对象内含被操作实体的 Prisma Client 模型。如果使用 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.GetAllQueryInput, Output async ( args, context ) { // ... }在客户端代码中使用 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()所有 CRUD 操作底层都是用 Queries 和 Actions 实现的因此天然具备它们的所有特性例如自动 SuperJSON 序列化以及使用 TypeScript 时的全栈类型安全。当前局限与未来规划Automatic CRUD 目前对业务逻辑的“认知”仍然有限具体体现在三个方面不理解实体间的归属关系例如它不知道“任务应归属于创建它的用户”这正是上文示例中需要覆盖create的原因。不感知授权规则例如它不知道“用户不应该为别的用户创建任务”。未来 Wasp 将加入基于角色的授权role-based authorization并计划让 CRUD 操作感知授权规则。缺少输入校验与清理例如无法自动保证任务描述不为空。CRUD 是一种快速搭建后端的手段它的能力上限取决于它从 Wasp 应用中获得的信息量应用能提供的信息越多开箱即用的能力就越强。官方计划持续支持并发展 CRUD使其成为创建后端最简单的方式并通过专门的 GitHub issue 跟踪进展。源码视角CRUD 是如何被解析与生成的在 Wasp 编译器中CRUD 功能的实现横跨“声明解析”和“代码生成”两个阶段可以从当前仓库的 Haskell 源码中一窥其原理。声明的数据结构waspc/src/Wasp/AppSpec/Crud.hs 定义了 CRUD 声明的核心数据结构Crud包含entity实体引用和operations操作集合两个字段CrudOperations恰好对应五种操作get、getAll、create、update、delete每个字段都是Maybe CrudOperationOptions——即只声明过的操作才会被生成未声明的操作为NothingCrudOperationOptions仅有两个可选字段isPublic :: Maybe Bool与overrideFn :: Maybe ExtImport与文档中的 API 完全对应toOperationList按固定顺序Get → GetAll → Create → Update → Delete把操作映射成列表供后续生成器使用。也就是说你在main.wasp里写的operations块会被解析为这份类型安全的 Haskell 数据结构作为后续代码生成的唯一依据。生成器如何产出服务端代码waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs 中的genCrud负责生成三部分文件路由索引src/routes/crud/index.ts汇总所有 CRUD 路由路由文件src/routes/crud/{name}.ts为每个 CRUD 生成路由模板数据中携带crud操作 JSON 和isAuthEnabled标志并通过getIdFieldFromCrudEntity获取实体主键字段即 Prisma 中id标记的字段操作文件src/crud/{name}.ts为每个 CRUD 生成操作定义模板数据包含crud操作 JSON、userEntityUpper、overrides覆盖实现的导入映射以及根据是否启用认证选择的AuthenticatedQueryDefinition/UnauthenticatedQueryDefinition/AuthenticatedActionDefinition/UnauthenticatedActionDefinition类型。这从源码层面印证了文档中的两个要点id 字段来自 Prisma 的id注解以及认证是否启用会直接影响生成操作的类型定义。测试对行为契约的锁定waspc/tests/Generator/CrudTest.hs 通过 Hspec 测试锁定了这些行为契约例如未定义任何操作时生成的 JSON 操作列表为空定义get/getAll/create三个操作时JSON 中恰好出现这三项且各操作对应路由为crud/tasks/get、crud/tasks/get-all、crud/tasks/createisPublic: true的操作在 JSON 中被标记为Public默认是NotPublic允许对操作进行overrideFn覆盖。这些测试与 waspc/src/Wasp/AppSpec/Crud.hs 的数据结构共同构成了 CRUD 声明的行为规范。仓库内的真实使用示例当前仓库的 kitchen-sink 示例examples/kitchen-sink/src/features/crud/crud.wasp.ts展示了新版 TypeScript spec 写法下的真实 CRUD 用法import { crud, page, route, type Spec } from wasp.sh/spec; import { crudCreateTask, crudGetAllTasks } from ./crud with { type: ref }; import { DetailPage } from ./pages/DetailPage with { type: ref }; import { ListPage } from ./pages/ListPage with { type: ref }; export const crudSpec: Spec [ crud(tasks, Task, { get: {}, getAll: { overrideFn: crudGetAllTasks, }, create: { overrideFn: crudCreateTask, }, update: {}, delete: {}, }), // Intentionally partial: only getAll is declared. Covers the case where the // generated CRUD must not mention the operations the user didnt ask for. crud(taskVotes, TaskVote, { getAll: {}, }), route(CrudListRoute, /crud, page(ListPage, { authRequired: true })), route(CrudDetailRoute, /crud/:id, page(DetailPage, { authRequired: true })), ];从这个示例可以看到两条值得注意的经验一部分操作使用默认实现get、update、delete另一部分用overrideFn覆盖getAll、create两者可以混用第二个crud(taskVotes, ...)故意只声明getAll一个操作专门验证“生成代码不会包含用户没有要求的操作”——这与CrudOperations数据结构中Maybe语义以及CrudTest.hs中的第一个测试用例相呼应。小结Automatic CRUD 是 Wasp 中“声明式后端”理念的集中体现一条crud声明即可获得针对实体的五种标准操作默认实现覆盖常规场景overrideFn提供业务规则兜底isPublic控制访问权限客户端通过wasp/client/crud以 Hook 形式直接消费并自动获得 SuperJSON 序列化与全栈类型安全。理解其默认实现的行为边界尤其是 create/update 会保存全部客户端数据这一点并在需要归属关系、授权规则或输入校验时果断使用overrideFn是把这个特性用好用对的关键。如果你想进一步探究可以阅读仓库中的以下文件官方文档本文依据的 version-0.14 CRUD 文档 与最新版 web/docs/data-model/crud.md实体与操作基础Entities、Queries、Actions、Operations 总览源码实现waspc/src/Wasp/AppSpec/Crud.hs、waspc/src/Wasp/Generator/ServerGenerator/CrudG.hs测试用例waspc/tests/Generator/CrudTest.hs真实示例examples/kitchen-sink/src/features/crud/crud.wasp.ts【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价