From e22547f46dda0f6314a560ecdbd7a43b2ff67c2d Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 31 Jul 2025 16:55:27 +0800 Subject: [PATCH] feat: sql resource --- packages/core/flow-engine/src/flowContext.ts | 31 ++++--- .../core/flow-engine/src/resources/index.ts | 1 + .../flow-engine/src/resources/sqlResource.ts | 89 +++++++++++++++++++ .../src/server/plugin.ts | 2 +- 4 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 packages/core/flow-engine/src/resources/sqlResource.ts diff --git a/packages/core/flow-engine/src/flowContext.ts b/packages/core/flow-engine/src/flowContext.ts index 7d784d15d40..afce2df8a8e 100644 --- a/packages/core/flow-engine/src/flowContext.ts +++ b/packages/core/flow-engine/src/flowContext.ts @@ -22,7 +22,7 @@ import { FlowEngine } from './flowEngine'; import { FlowI18n } from './flowI18n'; import { JSRunner, JSRunnerOptions } from './JSRunner'; import { FlowModel, ForkFlowModel } from './models'; -import { APIResource, BaseRecordResource, MultiRecordResource, SingleRecordResource } from './resources'; +import { APIResource, BaseRecordResource, MultiRecordResource, SingleRecordResource, SQLResource } from './resources'; import { FlowExitException, resolveDefaultParams, resolveExpressions } from './utils'; type Getter = (ctx: FlowContext) => T | Promise; @@ -618,19 +618,22 @@ export class FlowRuntimeContext< ) { super(); this.addDelegate(this.model.context); - const ResourceMap = { APIResource, BaseRecordResource, SingleRecordResource, MultiRecordResource }; - this.defineMethod('useResource', (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource') => { - if (model['resource']) { - return; - } - const R = ResourceMap[className]; - if (!R) { - throw new Error(`Resource class ${className} not found in ResourceMap`); - } - const resource = new R() as APIResource; - resource.setAPIClient(this.api); - model['resource'] = resource; - }); + const ResourceMap = { APIResource, BaseRecordResource, SingleRecordResource, MultiRecordResource, SQLResource }; + this.defineMethod( + 'useResource', + (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => { + if (model['resource']) { + return; + } + const R = ResourceMap[className]; + if (!R) { + throw new Error(`Resource class ${className} not found in ResourceMap`); + } + const resource = new R() as APIResource; + resource.setAPIClient(this.api); + model['resource'] = resource; + }, + ); this.defineProperty('resource', { get: () => model['resource'] || model.context['resource'], cache: false, diff --git a/packages/core/flow-engine/src/resources/index.ts b/packages/core/flow-engine/src/resources/index.ts index b0ec163d0a4..6c54b3f881e 100644 --- a/packages/core/flow-engine/src/resources/index.ts +++ b/packages/core/flow-engine/src/resources/index.ts @@ -12,4 +12,5 @@ export * from './baseRecordResource'; export * from './flowResource'; export * from './multiRecordResource'; export * from './singleRecordResource'; +export * from './sqlResource'; // diff --git a/packages/core/flow-engine/src/resources/sqlResource.ts b/packages/core/flow-engine/src/resources/sqlResource.ts new file mode 100644 index 00000000000..4688cba6d62 --- /dev/null +++ b/packages/core/flow-engine/src/resources/sqlResource.ts @@ -0,0 +1,89 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { observable } from '@formily/reactive'; +import _ from 'lodash'; +import { BaseRecordResource } from './baseRecordResource'; + +export class SQLResource extends BaseRecordResource { + protected _data = observable.ref(null); + protected _meta = observable.ref>({}); + private refreshTimer: NodeJS.Timeout | null = null; + + // 请求配置 - 与 APIClient 接口保持一致 + protected request = { + // url: null as string | null, + method: 'post' as string, + params: {} as Record, + data: {} as Record, + headers: {} as Record, + }; + + protected buildURL(action?: string): string { + return `flowSql:${action || 'run'}`; + } + + setType(type: 'selectRows' | 'selectRow' | 'selectVar') { + this.request.data.type = type; + return this; + } + + setFilterByTk(filterByTk: any) { + this.request.data.uid = filterByTk; + return this; + } + + setFilter(filter: Record) { + this.request.data.filter = filter; + return this; + } + + setSQL(sql: string) { + this.request.data.sql = sql; + return this; + } + + setBind(bind: Record | Array) { + this.request.data.bind = bind; + return this; + } + + /** + * 在同一个事件循环内多次调用 refresh 方法时,只有最后一次调用会生效。避免触发多次相同的接口请求。 + * @returns + */ + async refresh(): Promise { + // 清除之前的定时器,确保只有最后一次调用生效 + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + } + + // 设置新的定时器,在下一个事件循环执行 + return new Promise((resolve, reject) => { + this.refreshTimer = setTimeout(async () => { + try { + this.loading = true; + const { data, meta } = await this.runAction('run', { + method: 'post', + ...this.getRefreshRequestOptions(), + }); + this.setData(data).setMeta(meta); + this.emit('refresh'); + this.loading = false; + resolve(); + } catch (error) { + this.loading = false; + reject(error instanceof Error ? error : new Error(String(error))); + } finally { + this.refreshTimer = null; + } + }); + }); + } +} diff --git a/packages/plugins/@nocobase/plugin-collection-sql/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-collection-sql/src/server/plugin.ts index e9e09653333..0cd629e6c6e 100644 --- a/packages/plugins/@nocobase/plugin-collection-sql/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-collection-sql/src/server/plugin.ts @@ -33,7 +33,7 @@ export class PluginCollectionSQLServer extends Plugin { this.app.acl.allow('flowSql', 'run', 'loggedIn'); this.app.resourceManager.registerActionHandlers({ 'flowSql:run': async (ctx, next) => { - const { uid, sql, type, filter, bind } = ctx.action.params.values || {}; + const { uid, sql, type = 'selectRows', filter, bind } = ctx.action.params.values || {}; if (!uid) { ctx.throw(400, 'UID is required'); }