Metabase Data App 触发数据写入实战:useAction 与 defineAction 完整指南 📅 发布时间:2026/9/13 20:07:53 👁 浏览次数: Metabase Data App 触发数据写入实战useAction 与 defineAction 完整指南【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabaseMetabase 的 data app 运行在 Near Membrane 沙箱中原生fetch/XHR 到 Metabase 源站的调用会被直接拦截因此表单提交、更新行、删除条目、执行已保存的 action这类写操作只有一条正路通过metabase/embedding-sdk-react的useActionHook 调用实例上预定义好的 action。本篇基于仓库中的技能文档 skills/metabase-data-app-actions/SKILL.md 展开完整覆盖 action 的心智模型、typed schema 前置条件、useAction的全部返回值语义、标准建表表单示例、错误展示、参数映射与表单校验并结合仓库内 SDK 源码execute-action.ts、action.ts 及其单元测试印证底层调用链与权限机制读完即可在 data app 中正确实现用户点击 → 执行写操作 → 刷新界面数据的完整闭环。心智模型Action 属于 ModelData App 只负责触发Metabaseaction是服务端定义在数据仓库上的写操作分两类对某个 model 的基础 CRUD 操作insert / update / delete用户在 Metabase 中编写的自定义 SQL 命令。Action 的参数、model 绑定、权限都在 Metabase 实例上预先配置好。Data app 的职责是在用户执行某个交互点击按钮、提交表单、确认危险操作提示时用正确的参数调用这个已存在的 action。具体来说每个 action 都有一个父级 model在 schema 中表现为schema.models.modelName.actions.actionName。Model 条目出现在 schema 里只是为了给 action 编目不要把 model 当 question 渲染、不要把 model id 传给InteractiveQuestion、不要拉取 model 的行数据——读视图请使用 semantic-layer 的 query/question。action 的type要么是implicit对 model 的 CRUD要么是query用户编写的自定义 SQL。Implicit action 带有implicitKind声明它做什么row/create、row/update、row/delete或bulk/*变体。每个 action 发布一份parameters列表。每个参数有slugData app 提交时使用的键、jsTypestring/number/Date/boolean/unknown和可选的required标记。用action.parameters来决定渲染并提交哪些字段。create 结果里可能包含result[created-row]但该行的类型只是Recordstring, RowValue只拿它做轻量确认然后刷新页面上既有的表格/question/query 数据。不要去拉取或渲染父级 model 本身。Schema 前置条件actions 只在包含 models 时才生成Action 条目只有在 typed schema 包含 models 时才会生成。编写任何调用 action 的代码之前确认 schema 是用include-modelstrue生成的使用databasename-or-idinclude-modelstrue可以只为指定数据库包含 models/actions。不要依赖question-collections来拿 actions——question collections 只增加已保存的 question。写代码前先浏览 schema枚举你关心的各个 model 下schema.models.m.actions中有什么。schema 就是该实例 action 的完整目录catalog不是用来展示 model 数据的目录如果某个 model 的actions下有create、update、delete那这些就是可被调用的 action不存在的东西对 Data app 而言就是不存在。useAction Hook 逐项拆解import { useAction } from metabase/embedding-sdk-react; const { execute, isExecuting, result, error, reset } useAction(MyAction);参数规则——传defineAction(...)导出本身useAction(MyAction)的第一个参数是defineAction(...)的导出对象——传MyAction不要传MyAction.copiedActionId。生产构建会执行同步synchronized之后的 action 副本该副本的 model 位于 app 自己的 collection 中开发预览则继续执行原始 authored action因此 app 在从未同步过的状态下也能工作。不要传schema.models.model.actions.action或它的.id传 schema 条目是编译错误而原始 id 属于原始 modelapp 的用户无权读取生产环境会因权限调用失败。在 data app 之外Hook 也接受原始数值 id 或entity_id字符串。这一点在 SDK 源码中有直接印证。action.ts 中定义了defineAction导出的形状export type SdkActionDefinition { action: { id: SdkActionId }; copiedActionId?: number; };execute-action.ts 中的toExecutableActionId实现了环境切换逻辑dev 预览isDataAppDev或非 data app 环境直接使用input.action.id生产 data app 环境则要求input.copiedActionId存在否则抛出This action has not been synchronized. Run npm run sync-resources and rebuild.而在 data app 中传入原始 id 会直接抛出Action ${input} was passed to useAction as a raw id...。配套的单元测试 execute-action.unit.spec.ts 逐条验证了这四种行为生产构建执行副本、dev 预览执行原 action、data app 外执行原 action、未同步定义在生产中拒绝执行、data app 内拒绝原始 id。泛型规则——不要手写泛型defineAction导出自带 schema 条目因此 Hook 能从中同时推断出参数对象和带判别标签的resultparameters[]变成按键索引的对象required: true的条目是必选键各值按jsType定类型implicitKind/type变成 kindrow/create→createrow/update→updaterow/delete→delete任意bulk/*→bulktype query→sql。只有原始 id 形式才需要显式写泛型——useActionTParameters, TKind(42)——因为一个 id 什么信息都不携带。execute(parameters)— 触发 action。参数对象以参数的slug为键声明了required: true的参数是必选键其余可选。成功时返回响应体失败时抛出异常错误同时写入error状态供渲染层消费。当actionId为null或 SDK 尚未初始化时它不发起请求就 resolve 为null——如果这些情况在你的调用点可达请自行做守卫。没有enabled/options参数Hook 只在调用execute(...)时才运行门控选项是冗余的。要跳过 action就在事件处理器里分支const onClick async () { if (!user.canEdit) return; await execute({ id: orderId, discount }); };isExecuting— 在调用发起与 resolve 之间为true。用驱动按钮的disabled避免用户双击出重复请求。result— 响应体按TKind判别省略TKind时是AnyActionResult联合类型。首次调用之前和reset()之后为null。用于轻量确认insert 之后看result?.[created-row]SQL action 之后看result?.[rows-affected]但不要认为 create 行是类型丰富的 model 行——应该刷新周边数据见下文action 执行之后。error— 最近一次抛出的错误类型ActionExecuteError | null。无需 cast 直接读字段error?.data?.message、error?.status、error?.isCancelled。测试 execute-action.unit.spec.ts 中的用例验证了这个错误形状非 2xx 响应如 403会以{ status: 403, data: { message: denied } }的形式 reject与文档描述完全一致。reset()— 把result和error清回null。适合在用户确认成功或关闭错误之后调用。Hook 不会在挂载时自动触发——action 只在 Data app 显式调用execute(...)时运行。底层调用链从 Hook 到/api/action/:id/execute从源码结构看useAction最终落到 SDK bundle 的 executeAction 函数它通过 curried(store) fn形状挂载到window.METABASE_EMBEDDING_SDK_BUNDLE与createDashboard/queryQuestion同一机制并在给定 store 上 dispatchmetabase/api的 execute-action mutation。请求最终 POST 到/api/action/:id/executebody 为{ parameters: {...} }参数包在 SDK 层保持宽松类型数值合法性由服务端校验。测试用例进一步确认了parameters缺省时默认发送{}、以及entity_id字符串会路由到/api/action/:eid/execute。这也解释了为什么文档强调不要绕过 Hook 直接fetch(/api/action/...)沙箱会拦截原生网络调用只有data_app.yaml中声明的外部allowed_hosts允许裸fetch/XHRuseAction是唯一通路。标准用法创建一个表单来建行import { useAction } from metabase/embedding-sdk-react; import { CreatePerson } from ../../actions/people.action; function AddPersonForm({ onCreated }: { onCreated: () void }) { const { useState } React; const { execute, isExecuting, error, reset } useAction(CreatePerson); const [name, setName] useState(); const [email, setEmail] useState(); async function onSubmit(e: React.FormEvent) { e.preventDefault(); try { await execute({ name, email }); // 类型化键必须匹配参数 slug setName(); setEmail(); onCreated(); // ← 让父组件刷新依赖数据 } catch { // 错误已捕获进 Hook 状态供渲染层展示 } } return ( form onSubmit{onSubmit} input value{name} onChange{(e) setName(e.target.value)} / input value{email} onChange{(e) setEmail(e.target.value)} / button typesubmit disabled{isExecuting || !name || !email} {isExecuting ? Saving… : Add} /button /form ); }要点execute({ name, email })的键必须与参数slug一致成功后调用onCreated()让父组件数据 Hook 所在层刷新本组件只负责清空表单。错误信息的展示两层都要呈现当execute(...)失败时要呈现两层错误。Hook 的error类型为ActionExecuteError | null形状为{ status?, data: { message?, errors? }, isCancelled }。error.data.message是整请求级别的失败信息error.data.errors是按参数 slug 为键的逐字段校验映射{ slug: message }整请求失败时为{}。两层都直接读取无需 castconst fieldErrors error?.data.errors ?? {}; {error ? ( pre style{{ whiteSpace: pre-wrap, margin: 0 }} {error.data.message ?? Action failed.} /pre ) : null} {action.parameters.map((parameter) { const fieldError fieldErrors[parameter.slug]; return ( label key{parameter.slug} {parameter.displayName} input aria-invalid{Boolean(fieldError)} style{{ borderColor: fieldError ? #dc2626 : undefined }} / {fieldError ? div rolealert{fieldError}/div : null} /label ); })}用pre或任何white-space: pre-wrap的元素——消息里含有有意义的换行driver 错误会把 SQL 单独放一行。span会把它们压成一堵文字墙。举例把一个 9 字符的值提交进CHARACTER(2)列会得到Value too long for column STATE CHARACTER(2): dadasdasd (9); SQL statement: UPDATE PUBLIC.PEOPLE SET … WHERE PUBLIC.PEOPLE.ID 1 [22001-214]这一整串就是error.data.message原样渲染它。不要这样做渲染Failed/Something went wrong/String(error)代替error.data.message——用户将失去唯一能帮助他们修复的信息。校验失败时只渲染error.data.message——error.data.errors[parameter.slug]往往才是哪个字段、为什么的可操作细节。把消息转述成更友好的版本。原始的 H2/Postgres/SQL 错误比任何改写都更有行动价值。action 执行之后——保持 UI 诚实当 action 成功时用户看到的数据可能已经过期。没有显式刷新列表仍显示旧行统计卡仍显示旧计数用户刚编辑的行仍显示旧值。action 成功了但 UI 在撒谎——而且没有任何警告。规则简单而绝对action 成功 resolve 之后屏幕上任何可能被该 action 改变的数据都必须刷新。参数映射slug 是键jsType 决定输入控件schema 中 action 的每个parameters[]条目暴露slug、displayName、jsType和可选的required。Data app 要做的事execute({ … })的对象键匹配参数的slug字符串。把它们当作字面字符串键使用——当参数是defineAction(...)导出而非裸 id时Hook 从定义推导出的形状会在编译期拒绝拼写错误。根据jsType选对输入控件的type让浏览器提供正确的 UX 和内置类型转换。不要从 slug 名称臆测输入类型一个叫phone的 slug 仍然是jsType: string→typetextjsType输入控件stringinput typetext …numberinput typenumber …booleaninput typecheckbox …或 MantineSwitch/CheckboxDateinput typedate …如预期含时间分量则用datetime-localunknowninput typetext …——尽力而为在调用点做强制转换execute({ … })的值类型匹配jsType。jsType: number的参数要传number不是字符串。typenumber的输入对.valueAsNumber输出数字但input.value始终是字符串——如果你读的是后者在调用点做强制转换Number(input)。required: true的参数不可省略。这一点反映在 TS 类型上必选键就是必选的。displayName用于标签绝不用于键。对 implicit actionslug 与 model 的列名slug 化一致对自定义 SQL actionslug 是 action 作者在 Metabase 中给 SQL 参数起的名字。无论哪种情况schema 都是唯一事实来源。表单侧校验与主应用对齐schema 暴露parameter.required以及用于类型转换的jsType——这就是当前完整的校验契约与 Metabase 内置的 action 执行表单一致。没有长度检查、没有 min/max、没有格式检查——更细的校验会以 BE 错误在提交后返回见错误信息的展示。Schema 是唯一事实来源。读parameter.required并据此接线——仅此而已。不要因为字段名叫email就加上required不要凭感觉给 name 加 100 字符上限不要根据 slug 设typeemail。schema 沉默的地方字段就不受约束。表单无效时禁用提交按钮。用浏览器原生校验结果来驱动使required成为唯一生效的规则const [isFormValid, setIsFormValid] useState(false); const onFormChange (e: React.FormEventHTMLFormElement) setIsFormValid(e.currentTarget.checkValidity()); form onSubmit{...} onChange{onFormChange} input required{parameter.required} / button typesubmit disabled{isExecuting || !isFormValid}Create/button /form不要手写!name || !email之类的检查。调试清单当 action 看似成功但界面没更新或调用以 400 失败时在await execute(...)之后打印result、error、isExecuting确认请求确实发出并成功。确认useAction收到的是defineAction(...)导出。传 schema 条目是编译错误传它的.id能编译但execute失去类型且生产环境 403——因为 authored action 不是 app 用户有权执行的那个对应源码 execute-action.ts 的toExecutableActionId逻辑。打印传给execute({ ... })的对象。每个键必须匹配schema.models.model.actions.action.parameters中的某个参数slug每个值必须匹配其声明的jsType。列出屏幕上所有读取被改动 model 的数据视图。确认它们的数据 Hook 都挂载在 action 触发点之上这样其刷新回调才能向下传递。确认刷新回调在await execute(...)之后被调用且刷新本身也被 await。多个刷新并存时确认它们一起被 awaitPromise.all。对 implicit action确认正确的implicitKind与你的意图一致row/createvsrow/updatevsrow/delete——选错 action 就会发出错误的写。如果 schema 中没有你期望的 action要么该 action 未在实例上定义要么 schema 文件过期了。重新生成 schema。常见错误一览把schema.models.model.actions.action.id传给useAction而不是defineAction(...)导出。execute此后接受任意Recordstring, unknown{ wrongKey: 1 }这类拼写错误溜过编译期且生产环境 403——因为 authored action 不是 app 用户能运行的那个。成功之后忘记刷新。UI 继续渲染过期数据没有错误也没有警告。渲染Failed/Something went wrong/String(error)而非真实后端消息。永远提取error.data.message/error.data.errors用户需要的诊断就在里面并原样渲染。调用刷新回调时没有await。弹窗关闭或表单清空时新数据还没到用户短暂盯着过期视图。在触发 action 的同一个组件内部加载数据。Hook 位于触发点之下其刷新回调无法接线。把数据 Hook 提升到父组件把刷新回调传给触发点。参数jsType为number或boolean时传原始input字符串。在调用点强制转换。上一次请求未结束时让触发点再次触发。用 Hook 的isExecuting驱动disabled{isExecuting}。为了跳过刷新而把 action 的result直接前置塞进本地列表。服务端填充的默认值自增 ID、时间戳、计算列、派生 join 列与任何客户端猜测都会分叉。用户要求了实例没有暴露的行为于是发明一个不存在的 action。schema 是存在什么的目录——暴露这个缺口不要伪造调用。试图直接fetch(/api/action/...)。沙箱拦截到 Metabase 源站的裸网络调用裸fetch/XHR 只能到达 data_app.yaml 中声明的外部allowed_hosts通往 action 的唯一路径是useAction。延伸阅读本技能文档原文skills/metabase-data-app-actions/SKILL.mddata app 脚手架、data_app.yaml字段与沙箱限制skills/metabase-data-app-setup/SKILL.md 与模板 skills/metabase-data-app-setup/template/data_app.yamlSDK 侧 action 执行实现与类型execute-action.ts、types/action.ts、bundle 导出入口 sdk-bundle-exports.ts行为验证测试execute-action.unit.spec.ts适用前提说明data app 属于 Metabase 面向 64 版的 contenteditable="false">【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考