
NocoBase RunJS SQLResource 实战指南基于 SQL 模板与动态 SQL 的分页查询资源【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobaseSQLResource 是 NocoBase RunJS 运行环境中用于执行 SQL 查询的 Resource 类型数据来源为flowSql:run/flowSql:runById等后端接口。它不依赖具体数据表可直接执行已保存的 SQL 模板或调试模式的动态 SQL并原生支持分页、参数绑定、模板变量{{ctx.xxx}}与结果类型控制适用于报表统计、JSBlock 自定义列表、图表区块数据源等场景。读完本文你将掌握 SQLResource 的创建方式、两种执行模式、分页与事件机制并理解其背后的源码实现与权限边界。适用场景场景说明报表 / 统计复杂聚合、跨表查询、自定义统计指标JSBlock 自定义列表用 SQL 实现特殊筛选、排序或关联并自定义渲染图表区块保存 SQL 模板驱动图表数据源支持分页与 ctx.sql 的取舍需要分页、事件、响应式数据时用 SQLResource简单一次性查询可用ctx.sql.run()/ctx.sql.runById()从源码结构看SQLResource 在 sqlResource.ts 中与FlowSQLRepository即ctx.sql的实现类并列存在FlowSQLRepository提供一次性查询与模板管理而SQLResource在其之上叠加了分页、事件与响应式数据能力二者共享flowSql系列接口ctx.sql 文档 对此有详细说明。继承关系与创建方式继承关系FlowResource→APIResource→BaseRecordResource→SQLResource。对应的类定义位于 sqlResource.tsSQLResource、baseRecordResource.tsBaseRecordResource与 apiResource.tsAPIResource。创建方式ctx.makeResource(SQLResource)新建一个 resource 实例并返回不会写入或改变ctx.resource适合需要多个独立 resource 或临时查询的场景参见 ctx.makeResource()。ctx.initResource(SQLResource)若ctx.resource不存在则创建并绑定已存在则直接返回保证ctx.resource可用参见 ctx.initResource()。在 RunJS 中ctx.api由运行环境注入。SQLResource构造函数内部通过context.defineProperty(limit | offset, ...)向执行上下文注入了分页相关变量详见下文「分页」一节这是它与一次性ctx.sql查询的关键差异之一。数据格式getData()根据setSQLType()返回不同格式selectRows默认数组多行结果selectRow单条对象selectVar标量值如 COUNT、SUMgetMeta()返回分页等元信息page、pageSize、count、totalPage等在源码中SQLResource内部使用observable.ref维护_data与_meta见 sqlResource.ts因此getData()返回的数据是响应式的配合ctx.render可实现界面自动更新。refresh()完成后会调用this.setData(data).setMeta(meta)其中meta由底层runAction从接口响应中解析BaseRecordResource.runAction返回{ data, meta }结构。SQL 配置与执行模式方法说明setFilterByTk(uid)设置要执行的 SQL 模板 uid对应 runById需先在管理端保存setSQL(sql)设置原始 SQL仅调试模式setDebug(true)时用于 runBySQLsetSQLType(type)结果类型selectVar/selectRow/selectRowssetDebug(enabled)为 true 时 refresh 走runBySQL()否则走runById()run()根据 debug 调用runBySQL()或runById()runBySQL()用当前setSQL的 SQL 执行需先 setDebug(true)runById()用当前 uid 执行已保存的 SQL 模板执行模式的源码实现两种模式的分流逻辑在源码中非常清晰sqlResource.tsrefreshActionNamegetter 返回this._debugEnabled ? run : runByIdbuildURL(action)统一拼出flowSql:${action || this.refreshActionName}即 debug 模式请求flowSql:run正常模式请求flowSql:runByIdrun()方法即this._debugEnabled ? await this.runBySQL() : await this.runById()。runBySQL()的执行链路为transformSQL(sql)将{{ctx.xxx}}模板变量转换为绑定占位符→context.resolveJsonTemplate解析变量 →parseLiquidContext渲染最终 SQL → 通过runAction(run, ...)调用flowSql:run。其中transformSQL定义在 run-sql.ts它会把形如{{ctx.minId}}的表达式替换为$__varN占位符并生成对应的bind映射随后由 Liquid 引擎渲染为真实值从而在保留模板语法的同时避免 SQL 注入风险。runById()则分两步先通过runAction(getBind, { method: get, params: { uid } })调用flowSql:getBind拉取模板中保存的bind与liquidContext再用context.resolveJsonTemplate解析后调用flowSql:runById执行。调试模式与模板模式对比维度runBySQL调试runById模板触发方式setDebug(true)setSQL(sql)setFilterByTk(uid)后端接口flowSql:runflowSql:getBindflowSql:runById权限要求需 SQL 配置权限登录用户即可典型用途开发调试、动态 SQL生产环境复用已保存模板参数与上下文方法说明setBind(bind)绑定变量。对象形式配合:name数组形式配合?setLiquidContext(ctx)模板上下文Liquid用于解析{{ctx.xxx}}setFilter(filter)额外筛选条件传入请求 datasetDataSourceKey(key)数据源标识多数据源时使用从源码看这些 setter 大多写入this.request.data见 sqlResource.tssetBind写request.data.bind、setLiquidContext写request.data.liquidContext、setFilter写request.data.filter、setSQLType写request.data.type、setFilterByTk写request.data.uid。setDataSourceKey则写入request.data.dataSourceKey用于多数据源场景下的路由注意BaseRecordResource.setDataSourceKey的默认实现是写X-Data-Source请求头SQLResource 覆盖为该实现数据源标识随请求体传递二者行为以各自子类为准。关于参数绑定占位符ctx.sql文档sql.md中bind对象配合$name写法如WHERE status $status而 SQLResource 文档采用:name写法两者底层最终都通过flowSql接口的bind字段传递实际占位符语法取决于服务端 SQL 解析器。核心原则是使用setBind()配合占位符而不是字符串拼接这是防 SQL 注入的关键。分页方法说明setPage(page)/getPage()当前页默认 1setPageSize(size)/getPageSize()每页条数默认 20next()/previous()/goto(page)翻页并触发 refreshSQL 中可使用{{ctx.limit}}、{{ctx.offset}}引用分页参数SQLResource 会在上下文中注入limit、offset。分页参数的注入原理SQLResource构造函数通过context.defineProperty向执行上下文注入两个计算属性sqlResource.tslimit直接代理this.getPageSize()即每页条数默认 20offset计算为(page - 1) * pageSize即当前页的数据偏移量。因此只要在 SQL 中写LIMIT {{ctx.limit}} OFFSET {{ctx.offset}}或LIMIT {{ctx.limit}}配合OFFSETtransformSQL就会把模板变量转换为绑定占位符并注入正确数值。翻页方法next()/previous()/goto(page)的实现为next()将当前页加 1 后refresh()previous()仅在getPage() 1时减 1 并refresh()goto(page)仅在page 0时设置request.params.page后refresh()。setPage/setPageSize同时会setMeta({ page })/setMeta({ pageSize })使getMeta()能同步反映当前分页状态。数据拉取与事件方法说明refresh()执行 SQLrunById 或 runBySQL将结果写入setData(data)并更新 meta触发refresh事件runAction(actionName, options)调用底层接口如getBind、run、runByIdon(refresh, fn)/on(loading, fn)刷新完成、开始加载时触发refresh 防抖机制源码级refresh()在 sqlResource.ts 中实现了事件循环内合并每次调用都会先clearTimeout(this.refreshTimer)清除上一个定时器然后把当前调用的 resolve/reject 推入refreshWaiters队列再通过setTimeout(..., 0)把实际执行推迟到下一个事件循环。这样同一事件循环内多次调用refresh()时只有最后一次会真正发出接口请求而所有调用方的 Promise 都会在请求完成后统一 resolve/reject。该行为由单元测试验证见 sqlResource.test.ts测试使用vi.useFakeTimers()连续调用两次refresh()断言底层run只被调用 1 次且两个 Promise 都能正常 resolve失败场景下则断言两次调用都会 reject且run仍只调用 1 次。refresh()执行期间的状态流转为clearError()→loading true并emit(loading)→ 执行run()→setData(data).setMeta(meta)→loading false并emit(refresh)→ resolve 所有等待者异常时setError(error)并 reject 所有等待者。示例按已保存模板执行runByIdctx.initResource(SQLResource); ctx.resource.setFilterByTk(active-users-report); // 已保存的 SQL 模板 uid ctx.resource.setBind({ status: active }); await ctx.resource.refresh(); const data ctx.resource.getData(); const meta ctx.resource.getMeta(); // page、pageSize、count 等调试模式直接执行 SQLrunBySQLconst res ctx.makeResource(SQLResource); res.setDebug(true); res.setSQL(SELECT * FROM users WHERE status :status LIMIT {{ctx.limit}}); res.setBind({ status: active }); await res.refresh(); const data res.getData();分页与翻页ctx.resource.setFilterByTk(user-list-sql); ctx.resource.setPageSize(20); await ctx.resource.refresh(); // 翻页 await ctx.resource.next(); await ctx.resource.previous(); await ctx.resource.goto(3);结果类型// 多行默认 ctx.resource.setSQLType(selectRows); const rows ctx.resource.getData(); // [{...}, {...}] // 单行 ctx.resource.setSQLType(selectRow); const row ctx.resource.getData(); // {...} // 单值如 COUNT ctx.resource.setSQLType(selectVar); const total ctx.resource.getData(); // 42使用模板变量ctx.defineProperty(minId, { get: () 10 }); const res ctx.makeResource(SQLResource); res.setDebug(true); res.setSQL(SELECT * FROM users WHERE id {{ctx.minId}} LIMIT {{ctx.limit}}); await res.refresh();监听 refresh 事件ctx.resource?.on?.(refresh, () { const data ctx.resource.getData(); ctx.render(ul{data?.map((r) li key{r.id}{r.name}/li)}/ul); }); await ctx.resource?.refresh?.();底层接口flowSql 系列SQLResource 与ctx.sql共用同一组后端接口FlowSQLRepositorysqlResource.ts封装了完整的接口映射接口HTTP 方法用途flowSql:runPOST执行临时/动态 SQL调试模式flowSql:savePOST按 uid 保存/更新 SQL 模板flowSql:getBindGET按 uid 获取模板绑定的参数与 Liquid 上下文flowSql:runByIdPOST按 uid 执行已保存的 SQL 模板flowSql:destroyGETfilterByTk删除指定 uid 的 SQL 模板FlowSQLRepository还提供了ctx.sql.save({ uid, sql, dataSourceKey? })用于在 RunJS 中保存模板ctx.sql.runById(uid, options)用于简单一次性查询二者权限模型与 SQLResource 一致详见 ctx.sql。注意事项runById 需先保存模板setFilterByTk(uid)的 uid 必须是已在管理端保存的 SQL 模板 ID可通过ctx.sql.save({ uid, sql })保存。调试模式需权限setDebug(true)时走flowSql:run需当前角色具备 SQL 配置权限runById仅需登录即可。refresh 防抖同一事件循环内多次调用refresh()只会执行最后一次避免重复请求。参数绑定防注入使用setBind()配合:name/?或$name见 ctx.sql 文档占位符避免字符串拼接导致 SQL 注入。数据源路由多数据源环境下使用setDataSourceKey(key)指定目标数据源默认使用主数据源。相关文档ctx.sql - SQL 执行与管理ctx.sql.runById适合简单一次性查询ctx.resource - 当前上下文中的 resource 实例ctx.initResource() - 初始化并绑定到 ctx.resourcectx.makeResource() - 新建 resource 实例不绑定APIResource - 通用 API 资源MultiRecordResource - 面向数据表/列表【免费下载链接】nocobaseNocoBase is an open-source AI no-code platform for building business systems fast. Instead of generating everything from scratch, AI works on top of production-proven infrastructure and a WYSIWYG no-code interface, so you get both speed and reliability.项目地址: https://gitcode.com/GitHub_Trending/no/nocobase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考