)
Wasp Operations 详解用 Query 与 Action 构建全栈数据读写基于 version-0.19【免费下载链接】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 中Entities 负责定义应用的数据模型与关系而Operations操作则负责与这些数据交互读取、创建、更新。本文基于当前仓库中web/versioned_docs/version-0.19/data-model/operations/下的官方文档完整讲解 Operations 的两大成员——Queries查询与Actions动作——的声明、实现、调用方式、错误处理、基于 Entity 的自动缓存失效机制与全栈类型安全并结合waspc编译器的源码与examples/kitchen-sink示例工程进行原理佐证。读完本文你将能够在 Wasp 0.19 项目中熟练声明并落地自己的数据读写逻辑。一、什么是 Operations概述文档 开篇即给出核心定义Entities 让你定义数据模型和关系而 Operations 专注于操作这些数据。Operations 分为两类名字直接说明了它们的职责Queries用于读取数据例如获取博客文章的所有评论、点赞某视频的用户列表、根据 ID 查询单个商品信息。Actions用于修改数据更新已有记录或创建新记录例如给博客文章添加评论、给视频点赞、更新商品价格。在编译器层面这一区分被waspc的Operation数据类型固化了下来。查看 Operation.hs 可以看到-- | Common interface for queries and actions. data Operation QueryOp String Query | ActionOp String ActionOperation是 Query 与 Action 的统一抽象两者共享一套字段访问函数getName、getFn、getEntities、getAuth分别对应操作名、NodeJS 实现导入、关联的 Entity 列表、是否启用认证。这说明声明语法上二者高度同构差异主要体现在语义约定上——Query 只读、Action 可写。为什么需要这种抽象Wasp 的核心承诺是你只需要在.wasp文件中声明操作并在 NodeJS 中实现业务逻辑Wasp 就会自动生成一个服务端 NodeJS 函数与操作同名一个客户端 JavaScript 函数与操作同名调用时会把序列化后的参数通过网络传给服务端服务端一个HTTP API 路由处理器在底层调用操作的真实实现。也就是说你不需要手动搭建 HTTP API、处理服务端请求路由也不必操心客户端响应处理和缓存——Wasp 全部代劳。生成这两个同名的函数保证了全应用客户端与服务端拥有一致的调用接口。从源码看生成逻辑waspc中的路由生成器 OperationsRoutesG.hs 会为每个 Operation 生成独立的路由文件操作名.js放置在服务端源码目录下的operations/routes目录中。这印证了文档所述HTTP API route handler确实是编译期生成的产物而不是运行时动态注册的。二、创建并使用一个 Query要创建一个 Query只需两步在 Wasp 文件中用query声明编写 Query 的 NodeJS 实现。2.1 声明 Query在main.wasp中声明两个 Query一个获取全部任务一个按是否完成过滤任务// ... query getAllTasks { fn: import { getAllTasks } from src/queries } query getFilteredTasks { fn: import { getFilteredTasks } from src/queries }这里有两个要点Query 名称与实现函数名不必一致但官方建议保持一致以免混淆。声明可以提前于实现——文档特意指出先有高层概念Wasp 声明再写底层细节JS 实现是推荐的开发顺序。2.2 实现 QueryNodeJS把实现放在src/queries.{js,ts}中导出。先用纯内存数据演示// our database const tasks [ { id: 1, description: Buy some eggs, isDone: true }, { id: 2, description: Make an omelette, isDone: false }, { id: 3, description: Eat breakfast, isDone: false }, ] // You dont need to use the arguments if you dont need them export const getAllTasks () { return tasks } // The args object is something sent by the caller (most often from the client) export const getFilteredTasks (args) { const { isDone } args return tasks.filter((task) task.isDone isDone) }TypeScript 版本则可以借助 Wasp 自动生成的泛型类型获得完整类型支持import { type GetAllTasks, type GetFilteredTasks } from wasp/server/operations type Task { id: number description: string isDone: boolean } const tasks: Task[] [ { id: 1, description: Buy some eggs, isDone: true }, { id: 2, description: Make an omelette, isDone: false }, { id: 3, description: Eat breakfast, isDone: false }, ] export const getAllTasks: GetAllTasksvoid, Task[] () { return tasks } export const getFilteredTasks: GetFilteredTasks PickTask, isDone, Task[] (args) { const { isDone } args return tasks.filter((task) task.isDone isDone) }TypeScript 类型支持要点Wasp 会根据query声明自动生成同名泛型类型getAllTasks→GetAllTasksgetFilteredTasks→GetFilteredTasks从wasp/server/operations导入。泛型接受两个可选类型参数Inputargs载荷类型默认never与Output返回值类型默认unknown。不关心类型时可直接省略不希望接收/返回任何内容时用void。省略两个类型参数时 TypeScript 会推断为最宽松类型输入never、输出unknown仍可编译但会失去类型检查。若不想显式标注返回值类型可用satisfies关键字让 TypeScript 自动推断同时保证context类型正确const getFoo (async (_args, context) { const foos await context.entities.Foo.findMany() return { foos, message: Here are some foos!, queriedAt: new Date(), } }) satisfies GetFoo2.3 在客户端调用 Query从wasp/client/operations导入并直接调用无需关心该 Query 是否要求登录——Wasp 会在后台自动完成当前用户的认证import { getAllTasks, getFilteredTasks } from wasp/client/operations // TypeScript automatically infers the return values and type-checks the payloads. const allTasks await getAllTasks() const doneTasks await getFilteredTasks({ isDone: true })这就是全栈类型安全你只需在服务端实现处标注 Query 类型客户端代码便自动获得正确的参数与返回值类型两侧类型永远一致。2.4 在服务端调用 Query服务端调用与客户端几乎相同仅两点差异导入路径改为wasp/server/operations对需要认证的 Query必须显式传入带user字段的context对象其余部分如 Entities 会自动注入。import { getAllTasks, getFilteredTasks } from wasp/server/operations const user // Get an AuthUser object, e.g., from context.user in an operation. const allTasks await getAllTasks({ user }) const doneTasks await getFilteredTasks({ isDone: true }, { user })2.5 用useQueryHook 实现响应式数据在客户端Query 可以与useQueryHook 结合实现响应式。该 Hook 由 Wasp 内置是 react-query 的useQuery的轻量封装唯一区别是无需手动提供缓存 key——Wasp 在底层替你管理。import React from react import { type Task } from wasp/entities import { useQuery, getAllTasks, getFilteredTasks } from wasp/client/operations const MainPage () { // TypeScript automatically infers return values and type-checks payload types. const { data: allTasks, error: error1 } useQuery(getAllTasks) const { data: doneTasks, error: error2 } useQuery(getFilteredTasks, { isDone: true, }) if (error1 ! null || error2 ! null) { return divThere was an error/div } return ( div h2All Tasks/h2 {allTasks allTasks.length 0 ? allTasks.map((task) Task key{task.id} {...task} /) : No tasks} h2Finished Tasks/h2 {doneTasks doneTasks.length 0 ? doneTasks.map((task) Task key{task.id} {...task} /) : No finished tasks} /div ) }注意这里不需要给useQuery(getAllTasks)的结果标注类型——Wasp 已从服务端实现推断出返回类型这正是全栈类型安全的体现。三、创建并使用一个 ActionActions 的 API 与 Queries 几乎一致关键区别在于语义Actions 用于修改/新增数据Queries 只读数据。二者协同工作共同维持数据缓存的新鲜度。3.1 声明 Action// ... action createTask { fn: import { createTask } from src/actions } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions }3.2 实现 Action实现文件为src/actions.{js,ts}// our database let nextId 4 const tasks [ { id: 1, description: Buy some eggs, isDone: true }, { id: 2, description: Make an omelette, isDone: false }, { id: 3, description: Eat breakfast, isDone: false }, ] export const createTask (args) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } export const markTaskAsDone (args) { const task tasks.find((task) task.id args.id) if (!task) { return } task.isDone true }TypeScript 版本同样借助自动生成的CreateTask、MarkTaskAsDone泛型类型import { type CreateTask, type MarkTaskAsDone } from wasp/server/operations type Task { id: number description: string isDone: boolean } let nextId 4 const tasks: Task[] [ { id: 1, description: Buy some eggs, isDone: true }, { id: 2, description: Make an omelette, isDone: false }, { id: 3, description: Eat breakfast, isDone: false }, ] export const createTask: CreateTaskPickTask, description, Task ( args ) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { const task tasks.find((task) task.id args.id) if (!task) { return } task.isDone true }3.3 在客户端调用 Actionimport { createTask, markTaskAsDone } from wasp/client/operations // TypeScript automatically infers the return values and type-checks the payloads. const newTask await createTask({ description: Keep learning TypeScript }) await markTaskAsDone({ id: 1 })Actions 不需要响应式因此可以直接在组件里调用无需 Hookimport React from react import { useQuery, getTask, markTaskAsDone } from wasp/client/operations export const TaskPage ({ id }: { id: number }) { const { data: task } useQuery(getTask, { id }) if (!task) { return h1Loading/h1 } const { description, isDone } task return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p {isDone || ( button onClick{() markTaskAsDone({ id })}Mark as done./button )} /div ) }3.4 在服务端调用 Action与 Query 相同导入路径换成wasp/server/operations并为认证 Action 传入带user的 contextimport { createTask, markTaskAsDone } from wasp/server/operations const user // Get an AuthUser object, e.g., from context.user const newTask await createTask( { description: Keep learning TypeScript }, { user }, ) await markTaskAsDone({ id: 1 }, { user })四、错误处理HttpError与安全默认值出于安全考虑Query/Action 实现中抛出的所有异常都会以 HTTP500返回给客户端且移除全部细节——防止敏感信息泄露到网络。如果你确实需要向客户端传递错误信息可以在实现中构造并抛出HttpErrorimport { type GetAllTasks } from wasp/server/operations import { HttpError } from wasp/server export const getAllTasks: GetAllTasks async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }规则如下状态码为4xx时客户端会收到包含对应message与data字段的响应对象并将错误含这些字段重新抛出其他状态码下服务端不会转发这些字段以避免信息泄露。在真实项目中这一模式被广泛用于认证与授权检查。例如examples/kitchen-sink的 actions.ts 中createTask在未登录时直接throw new HttpError(401)登录后才通过context.user.id关联新建任务。五、在 Operation 中使用 Entities接入 Prisma大多数情况下Operation 操作的数据是 Entities即schema.prisma中定义的 Prisma model。使用方式在声明中把实体加入entities: [Task]列表Wasp 会将其注入到实现的context参数中query getAllTasks { fn: import { getAllTasks } from src/queries, entities: [Task] } query getFilteredTasks { fn: import { getFilteredTasks } from src/queries, entities: [Task] }然后在实现里通过context.entities.Task访问 Prisma 的 CRUD APIimport { type Task } from wasp/entities import { type GetAllTasks, type GetFilteredTasks } from wasp/server/operations export const getAllTasks: GetAllTasksvoid, Task[] async (args, context) { return context.entities.Task.findMany({}) } export const getFilteredTasks: GetFilteredTasks PickTask, isDone, Task[] async (args, context) { return context.entities.Task.findMany({ where: { isDone: args.isDone }, }) }Action 同理使用create/update等方法import { type CreateTask, type MarkTaskAsDone } from wasp/server/operations import { type Task } from wasp/entities export const createTask: CreateTaskPickTask, description, Task async ( args, context ) { const newTask await context.entities.Task.create({ data: { description: args.description, isDone: false, }, }) return newTask } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void async ( args, context ) { await context.entities.Task.update({ where: { id: args.id }, data: { isDone: true }, }) }context.entities.Task暴露的正是 Prisma 的prisma.taskCRUD API。从编译器源码看Query与Action记录类型都包含entities :: Maybe [Ref Entity]与auth :: Maybe Bool两个可选字段见 Query.hs 与 Operation.hsEntity 列表与认证开关都是在声明阶段被静态解析并注入到生成代码中的。载荷约束superjson在实现 Operation 前需要了解一个底层细节Wasp 内部使用 superjson。在 TypeScript 中只要用 Wasp 自动生成的类型标注 Operation编译器就会确保载荷是 Wasp 可序列化的。六、基于 Entity 的自动缓存失效Cache InvalidationWeb 应用状态管理中最棘手的问题之一是确保 Query 返回的数据始终是最新的。Wasp 用 react-query 管理 Query 缓存因此必须保证数据变陈旧时能及时失效。虽然可以借助 react-query 提供的机制refetch、直接 invalidation手动失效缓存但手动管理很快会变得复杂且易出错。Wasp 提供了更高效的默认方案基于 Entity 的自动缓存失效。核心规则Actions 修改数据Queries 读取数据当一个Action 使用的 Entity与某个Query 使用的 Entity相同执行该 Action 时Wasp 会自动失效对应 Query 的缓存触发其重新从服务端拉取数据。举例若 ActioncreateTask与 QuerygetTasks都使用 EntityTask那么执行createTask后getTasks的缓存可能已过期Wasp 会将其失效并自动 refetch。这意味着你不必为缓存失效费心Queries 始终保持新鲜。反面来说这种自动失效可能带来一些不必要的更新并非每次 Action 执行都真正影响数据且只对 Entities 生效。如果你需要更精细的控制可以回退到 react-query 的手动机制Wasp 目前原生支持的手动机制只有乐观更新见下文其余场景可依赖 react-query。七、useActionHook 与乐观更新Optimistic Updates虽然 Actions 不要求响应式、可直接调用但 Wasp 提供了useActionHook 来装饰Action——它返回一个 API 与原 Action 一致、但底层附带额外行为的函数。useAction接受两个参数actionFn必填要增强的 Wasp Action客户端生成的 Action 函数。actionOptions配置额外特性的对象。技术上可选但不传它等于直接使用原 Action没有意义。目前仅支持一个字段optimisticUpdates乐观更新定义数组每个对象包含getQuerySpecifier必填返回 Query specifier 的函数。Query specifier 是一个数组由 query 函数与参数组成。例如要乐观更新useQuery(fetchFilteredTasks, { isDone: true })的缓存就返回[fetchFilteredTasks, { isDone: true }]。Wasp 会把传给装饰后 Action 的参数转发给此函数便于你利用新增/修改项的属性定位 Query。updateQuery必填执行乐观更新的纯函数返回缓存的目标状态。Wasp 以两个参数调用它item传入装饰后 Action 的参数与oldData当前缓存值。示例让markTaskAsDone在点击完成按钮后立即把getTask的缓存置为isDone: true无需等待服务端响应import React from react import { useQuery, useAction, type OptimisticUpdateDefinition, getTask, markTaskAsDone, } from wasp/client/operations type TaskPayload PickTask, id; const TaskPage ({ id }: { id: number }) { const { data: task } useQuery(getTask, { id }); // Typescript automatically type-checks the payload type. const markTaskAsDoneOptimistically useAction(markTaskAsDone, { optimisticUpdates: [ { getQuerySpecifier: ({ id }) [getTask, { id }], updateQuery: (_payload, oldData) ({ ...oldData, isDone: true }), } as OptimisticUpdateDefinitionTaskPayload, Task, ], }); if (!task) { return h1Loading/h1; } const { description, isDone } task; return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p {isDone || ( button onClick{() markTaskAsDoneOptimistically({ id })} Mark as done. /button )} /div ); };使用updateQuery时必须遵守三条纪律它必须是纯函数只返回getQuerySpecifier定位到的缓存目标值不得有任何副作用只更新确实受该 Action 影响的 Query 缓存Wasp 目前还无法自动校验这一点实现要对oldData的任意状态都健壮例如不要依赖数组元素的固定位置。高级用法直接使用 react-queryWasp 的乐观更新 API 刻意保持小而专一只聚焦于 Query 缓存更新。如果你需要更高自由度可以放弃useAction直接使用 react-query 的useMutation及其底层 API。此时你需要拿到 Query 的缓存 key——Wasp 内部使用它但对外抽象掉了。解决办法任何 Query 都暴露queryCacheKey属性import { getTasks } from wasp/client/operations const queryKey getTasks.queryCacheKey八、API 参考速查query声明支持的字段fn: ExtImport必填Query 的 NodeJS 实现导入语句例如fn: import { getFoo } from src/queries。entities: [Entity]可选在 Query 内部使用的 Entity 列表会注入到context.entities。声明后// 客户端使用 import { getFoo } from wasp/client/operations // 服务端使用 import { getFoo } from wasp/server/operations // 服务端类型 import { type GetFoo } from wasp/server/operationsQuery/Action 实现函数的签名实现是一个 NodeJS 函数需要await时可声明为async接收两个位置参数参数名随意文档惯例为args与contextargs调用时传入的数据对象如过滤条件类型取决于操作contextWasp 注入的上下文对象包含用户会话信息context.user与实体信息context.entities。实体用法见上文在 Operation 中使用 Entitiesuser对象的用法可参考认证文档中关于context.user的部分。useQueryHook 参数queryFn必填Wasp 基于query声明生成的客户端查询函数queryFnArgs传给 Query 的参数对象最终成为实现函数的第一个位置参数optionsreact-query 的 options 对象用于调整该 Query 的默认行为若需修改全局默认值可在 客户端配置 的 setup 函数中设置。Queries 与 Actions 的差异总结Actions 可以且通常应该修改服务端状态Queries 只允许读取。Wasp 的自动缓存失效依赖这一约定必须遵守Actions 不需要响应式可直接调用但可用useActionHook 附加额外行为如乐观更新action声明与query声明几乎完全相同唯一区别在于声明关键字。九、仓库中的真实范例examples/kitchen-sink是官方示例应用其中actions.ts 展示了带认证检查的 Action 实现未登录抛HttpError(401)、登录后通过context.user.id关联数据examples/kitchen-sink/src/features/operations/目录下还包含缓存失效相关的测试cacheInvalidation.test.ts可直接阅读以理解自动缓存失效的预期行为examples/ask-the-documents、examples/waspello等示例应用的main.wasp.ts中均有大量query/action声明可作为声明语法的真实参考。总结Operations 是 Wasp 数据读写能力的统一入口Query 声明 NodeJS 实现 全栈可调用的只读接口Action 声明 NodeJS 实现 全栈可调用的写接口。配合 Entity 注入、基于 Entity 的自动缓存失效、useAction乐观更新以及自动生成类型带来的全栈类型安全Wasp 把传统全栈应用中大量重复的胶水代码HTTP 路由、序列化、认证注入、缓存管理收敛到了编译期。继续阅读 Queries 完整指南 与 Actions 完整指南可以获取更详尽的示例与边界行为说明。【免费下载链接】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),仅供参考