@nocobase/flow-engine 2.0.0-beta.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. package/src/views/viewEvents.ts +55 -0
@@ -8,9 +8,11 @@
8
8
  */
9
9
 
10
10
  import { describe, expect, it, vi } from 'vitest';
11
- import { FlowContext, FlowRuntimeContext } from '../flowContext';
11
+ import { FlowContext, FlowRuntimeContext, FlowRunJSContext, type PropertyMetaFactory } from '../flowContext';
12
12
  import { FlowEngine } from '../flowEngine';
13
13
  import { FlowModel } from '../models/flowModel';
14
+ import { RunJSContextRegistry } from '../runjs-context/registry';
15
+ import { setupRunJSContexts } from '../runjs-context/setup';
14
16
 
15
17
  describe('FlowContext properties and methods', () => {
16
18
  it('should return static property value', () => {
@@ -752,6 +754,590 @@ describe('FlowContext properties and methods', () => {
752
754
  });
753
755
  });
754
756
 
757
+ describe('FlowContext.getApiInfos', () => {
758
+ it('should support defineMethod info (string as description)', async () => {
759
+ const ctx = new FlowContext();
760
+ ctx.defineMethod('foo', () => 1, 'do something');
761
+ expect(ctx.foo()).toBe(1);
762
+
763
+ const infos = await ctx.getApiInfos();
764
+ expect(infos.foo?.description).toBe('do something');
765
+ expect(infos.foo?.type).toBe('function');
766
+ });
767
+
768
+ it('should support defineMethod info object (completion/ref)', async () => {
769
+ const ctx = new FlowContext();
770
+ ctx.defineMethod('bar', () => 2, {
771
+ description: 'Bar',
772
+ completion: { insertText: 'ctx.bar()' },
773
+ ref: { url: 'https://example.com', title: 'Docs' },
774
+ });
775
+
776
+ const infos = await ctx.getApiInfos();
777
+ expect((infos.bar as any)?.completion).toBeUndefined();
778
+ expect((infos.bar?.ref as any)?.url).toBe('https://example.com');
779
+ });
780
+
781
+ it('should return property infos with completion/ref/examples', async () => {
782
+ const ctx = new FlowContext();
783
+ ctx.defineProperty('token', {
784
+ value: 't',
785
+ meta: {
786
+ type: 'string',
787
+ title: 'Token',
788
+ },
789
+ info: {
790
+ description: 'Token string',
791
+ completion: { insertText: 'ctx.token' },
792
+ ref: 'docs/token',
793
+ examples: ['const t = ctx.token'],
794
+ },
795
+ });
796
+
797
+ const infos = await ctx.getApiInfos();
798
+ expect((infos.token as any)?.completion).toBeUndefined();
799
+ expect(infos.token?.title).toBeUndefined();
800
+ expect(infos.token?.type).toBeUndefined();
801
+ expect(infos.token?.ref).toBe('docs/token');
802
+ expect(infos.token?.examples).toContain('const t = ctx.token');
803
+ });
804
+
805
+ it('should not return meta-only properties and should not trigger meta factory', async () => {
806
+ const ctx = new FlowContext();
807
+ const metaFactory = vi.fn(() => ({ type: 'string', title: 'MetaOnly' })) as any;
808
+ ctx.defineProperty('metaOnly', { meta: metaFactory });
809
+
810
+ const infos = await ctx.getApiInfos();
811
+ expect(infos.metaOnly).toBeUndefined();
812
+ expect(metaFactory).not.toHaveBeenCalled();
813
+ });
814
+
815
+ it('should not return keys starting with underscore', async () => {
816
+ class TestRunJSContext extends FlowRunJSContext {}
817
+ TestRunJSContext.define({
818
+ properties: {
819
+ _internal: 'internal',
820
+ pub: {
821
+ type: 'object',
822
+ description: 'pub',
823
+ properties: {
824
+ _secret: 'secret',
825
+ visible: 'visible',
826
+ },
827
+ },
828
+ },
829
+ methods: {
830
+ _m: 'hidden method',
831
+ m: 'visible method',
832
+ },
833
+ });
834
+ RunJSContextRegistry.register('test-private', '*', TestRunJSContext as any);
835
+
836
+ const ctx = new FlowContext();
837
+ ctx.defineProperty('_runtimeSecret', {
838
+ info: { description: '_runtimeSecret' },
839
+ });
840
+ ctx.defineMethod('_runtimeMethod', () => 1, 'runtime hidden');
841
+
842
+ const infos = await ctx.getApiInfos({ version: 'test-private' as any });
843
+ expect(infos._internal).toBeUndefined();
844
+ expect(infos._runtimeSecret).toBeUndefined();
845
+ expect(infos._m).toBeUndefined();
846
+ expect(infos._runtimeMethod).toBeUndefined();
847
+
848
+ expect(infos.pub).toBeTruthy();
849
+ expect(infos.pub?.properties).toBeUndefined();
850
+ expect(infos.m).toBeTruthy();
851
+ });
852
+
853
+ it('should only return top-level keys and no completion', async () => {
854
+ class TestRunJSContext extends FlowRunJSContext {}
855
+ TestRunJSContext.define({
856
+ properties: {
857
+ api: {
858
+ type: 'object',
859
+ description: 'api',
860
+ properties: {
861
+ request: { type: 'function', description: 'request' },
862
+ },
863
+ },
864
+ },
865
+ });
866
+ RunJSContextRegistry.register('test-layer', '*', TestRunJSContext as any);
867
+
868
+ const ctx = new FlowContext();
869
+ const infos = await ctx.getApiInfos({ version: 'test-layer' as any });
870
+ expect(infos.api).toBeTruthy();
871
+ expect(infos.api?.properties).toBeUndefined();
872
+ expect((infos.api as any)?.completion).toBeUndefined();
873
+ expect((infos as any).request).toBeUndefined();
874
+ });
875
+
876
+ it('should exclude variable-like roots and expose libs docs under libs.*', async () => {
877
+ class TestRunJSContext extends FlowRunJSContext {}
878
+ TestRunJSContext.define({
879
+ properties: {
880
+ record: 'Current record (var-like).',
881
+ formValues: 'Form values (var-like).',
882
+ popup: {
883
+ description: 'Popup context (var-like).',
884
+ detail: 'Promise<any>',
885
+ },
886
+ React: 'React namespace. Recommended access path: `ctx.libs.React`.',
887
+ ReactDOM: 'ReactDOM API. Recommended access path: `ctx.libs.ReactDOM`.',
888
+ antd: 'Ant Design. Recommended access path: `ctx.libs.antd`.',
889
+ libs: {
890
+ description: 'Libraries namespace.',
891
+ properties: {
892
+ antdIcons: 'Ant Design icons library. Example: `ctx.libs.antdIcons.PlusOutlined`.',
893
+ },
894
+ },
895
+ },
896
+ });
897
+ RunJSContextRegistry.register('test-api-libs' as any, '*', TestRunJSContext as any);
898
+
899
+ const ctx = new FlowContext();
900
+ const infos = await ctx.getApiInfos({ version: 'test-api-libs' as any });
901
+
902
+ // Variable-like roots documented in RunJS doc should be served by getVarInfos/getEnvInfos.
903
+ expect(infos.record).toBeUndefined();
904
+ expect(infos.formValues).toBeUndefined();
905
+ expect(infos.popup).toBeUndefined();
906
+
907
+ // Avoid exposing global lib aliases; document them under ctx.libs.* instead.
908
+ expect((infos as any).React).toBeUndefined();
909
+ expect((infos as any).ReactDOM).toBeUndefined();
910
+ expect((infos as any).antd).toBeUndefined();
911
+
912
+ expect(infos['libs.React']?.description).toContain('ctx.libs.React');
913
+ expect(infos['libs.ReactDOM']?.description).toContain('ctx.libs.ReactDOM');
914
+ expect(infos['libs.antd']?.description).toContain('ctx.libs.antd');
915
+ expect(infos['libs.antdIcons']?.description).toContain('antdIcons');
916
+ });
917
+
918
+ it('should include getApiInfos/getVarInfos/getEnvInfos in api infos output', async () => {
919
+ await setupRunJSContexts();
920
+ const ctx = new FlowContext();
921
+ const infos = await ctx.getApiInfos();
922
+
923
+ expect(infos.getApiInfos?.type).toBe('function');
924
+ expect(String(infos.getApiInfos?.description || '')).toMatch(/\S/);
925
+
926
+ expect(infos.getVarInfos?.type).toBe('function');
927
+ expect(String(infos.getVarInfos?.description || '')).toMatch(/\S/);
928
+
929
+ expect(infos.getEnvInfos?.type).toBe('function');
930
+ expect(String(infos.getEnvInfos?.description || '')).toMatch(/\S/);
931
+ });
932
+ });
933
+
934
+ describe('FlowContext.getVarInfos', () => {
935
+ it('should respect maxDepth when expanding properties', async () => {
936
+ const ctx = new FlowContext();
937
+ ctx.defineProperty('deep', {
938
+ meta: {
939
+ type: 'object',
940
+ title: 'deep',
941
+ properties: {
942
+ level1: {
943
+ type: 'object',
944
+ title: 'l1',
945
+ properties: {
946
+ level2: {
947
+ type: 'object',
948
+ title: 'l2',
949
+ properties: {
950
+ level3: { type: 'string', title: 'l3' },
951
+ },
952
+ },
953
+ },
954
+ },
955
+ },
956
+ },
957
+ });
958
+
959
+ const vars = await ctx.getVarInfos({ path: 'deep', maxDepth: 2 });
960
+ expect(vars.deep?.properties?.level1).toBeTruthy();
961
+ expect(vars.deep?.properties?.level1?.properties?.level2).toBeUndefined();
962
+ });
963
+
964
+ it('should support deep path pruning (root key is the path string)', async () => {
965
+ const ctx = new FlowContext();
966
+ ctx.defineProperty('deep', {
967
+ meta: {
968
+ type: 'object',
969
+ title: 'deep',
970
+ properties: {
971
+ level1: {
972
+ type: 'object',
973
+ title: 'l1',
974
+ properties: {
975
+ level2: { type: 'string', title: 'l2' },
976
+ },
977
+ },
978
+ },
979
+ },
980
+ });
981
+
982
+ const vars = await ctx.getVarInfos({ path: 'deep.level1', maxDepth: 2 });
983
+ expect(vars['deep.level1']?.title).toBe('l1');
984
+ expect(vars['deep.level1']?.properties?.level2).toBeTruthy();
985
+ });
986
+
987
+ it('should expand PropertyMetaFactory nodes in path pruning', async () => {
988
+ const ctx = new FlowContext();
989
+
990
+ const recordFactory: PropertyMetaFactory = async () => ({
991
+ type: 'object',
992
+ title: 'Record',
993
+ properties: {
994
+ id: { type: 'string', title: 'id' },
995
+ },
996
+ });
997
+ recordFactory.title = 'Record';
998
+ recordFactory.hasChildren = true;
999
+
1000
+ ctx.defineProperty('popup', {
1001
+ meta: () => ({
1002
+ type: 'object',
1003
+ title: 'popup',
1004
+ properties: async () => ({
1005
+ record: recordFactory as any,
1006
+ }),
1007
+ }),
1008
+ });
1009
+
1010
+ const vars = await ctx.getVarInfos({ path: 'popup.record', maxDepth: 3 });
1011
+ expect(vars['popup.record']?.title).toBe('Record');
1012
+ expect(vars['popup.record']?.properties?.id).toBeTruthy();
1013
+ });
1014
+
1015
+ it('should support deep path pruning through PropertyMetaFactory nodes', async () => {
1016
+ const ctx = new FlowContext();
1017
+
1018
+ const recordFactory: PropertyMetaFactory = async () => ({
1019
+ type: 'object',
1020
+ title: 'Record',
1021
+ properties: {
1022
+ id: { type: 'string', title: 'id' },
1023
+ },
1024
+ });
1025
+ recordFactory.title = 'Record';
1026
+ recordFactory.hasChildren = true;
1027
+
1028
+ ctx.defineProperty('popup', {
1029
+ meta: () => ({
1030
+ type: 'object',
1031
+ title: 'popup',
1032
+ properties: async () => ({
1033
+ record: recordFactory as any,
1034
+ }),
1035
+ }),
1036
+ });
1037
+
1038
+ const vars = await ctx.getVarInfos({ path: 'popup.record.id', maxDepth: 2 });
1039
+ expect(vars['popup.record.id']?.title).toBe('id');
1040
+ expect(vars['popup.record.id']?.type).toBe('string');
1041
+ });
1042
+
1043
+ it('should expand PropertyMetaFactory children in full output', async () => {
1044
+ const ctx = new FlowContext();
1045
+
1046
+ const recordFactory: PropertyMetaFactory = async () => ({
1047
+ type: 'object',
1048
+ title: 'Record',
1049
+ properties: {
1050
+ id: { type: 'string', title: 'id' },
1051
+ },
1052
+ });
1053
+ recordFactory.title = 'Record';
1054
+ recordFactory.hasChildren = true;
1055
+
1056
+ ctx.defineProperty('popup', {
1057
+ meta: () => ({
1058
+ type: 'object',
1059
+ title: 'popup',
1060
+ properties: async () => ({
1061
+ record: recordFactory as any,
1062
+ }),
1063
+ }),
1064
+ });
1065
+
1066
+ const vars = await ctx.getVarInfos({ maxDepth: 3 });
1067
+ expect(vars.popup?.properties?.record?.properties?.id).toBeTruthy();
1068
+ });
1069
+
1070
+ it('should compute/skip hidden nodes and compute disabled/disabledReason', async () => {
1071
+ const ctx = new FlowContext();
1072
+ ctx.defineProperty('secret', {
1073
+ meta: { type: 'string', title: 'secret', hidden: () => true },
1074
+ });
1075
+ ctx.defineProperty('disabledProp', {
1076
+ meta: {
1077
+ type: 'string',
1078
+ title: 'disabledProp',
1079
+ disabled: async () => true,
1080
+ disabledReason: async () => 'not available',
1081
+ },
1082
+ });
1083
+
1084
+ const vars = await ctx.getVarInfos();
1085
+ expect(vars.secret).toBeUndefined();
1086
+ expect(vars.disabledProp?.disabled).toBe(true);
1087
+ expect(vars.disabledProp?.disabledReason).toBe('not available');
1088
+ });
1089
+
1090
+ it('should not return empty property infos (no meta)', async () => {
1091
+ const ctx = new FlowContext();
1092
+ ctx.defineProperty('plainProp', { value: 1 });
1093
+ ctx.defineMethod('plainMethod', () => 1);
1094
+
1095
+ const vars = await ctx.getVarInfos();
1096
+ expect(vars.plainProp).toBeUndefined();
1097
+ expect(vars.plainMethod).toBeUndefined();
1098
+ });
1099
+
1100
+ it('should not return keys starting with underscore (including nested)', async () => {
1101
+ const ctx = new FlowContext();
1102
+ ctx.defineProperty('_runtimeSecret', {
1103
+ meta: { type: 'string', title: '_runtimeSecret' },
1104
+ });
1105
+ ctx.defineProperty('runtime', {
1106
+ meta: {
1107
+ type: 'object',
1108
+ title: 'runtime',
1109
+ properties: {
1110
+ _child: { type: 'string', title: '_child' },
1111
+ child: { type: 'string', title: 'child' },
1112
+ },
1113
+ },
1114
+ });
1115
+
1116
+ const vars = await ctx.getVarInfos({ maxDepth: 3 });
1117
+ expect(vars._runtimeSecret).toBeUndefined();
1118
+ expect(vars.runtime?.properties?._child).toBeUndefined();
1119
+ expect(vars.runtime?.properties?.child).toBeTruthy();
1120
+ });
1121
+ });
1122
+
1123
+ describe('FlowContext.getEnvInfos', () => {
1124
+ it('should return envs nodes (popup/block/resource/record)', async () => {
1125
+ const ctx = new FlowContext();
1126
+ ctx.defineProperty('blockModel', { value: { uid: 'b1' } });
1127
+ ctx.defineProperty('popup', {
1128
+ value: Promise.resolve({
1129
+ uid: 'p1',
1130
+ resource: {
1131
+ collectionName: 'posts',
1132
+ dataSourceKey: 'main',
1133
+ associationName: 'comments',
1134
+ filterByTk: 1,
1135
+ sourceId: 2,
1136
+ },
1137
+ }),
1138
+ });
1139
+
1140
+ const envs = await ctx.getEnvInfos();
1141
+
1142
+ expect(envs.resource?.getVar).toBe('ctx.popup.resource');
1143
+ expect(envs.resource?.properties?.collectionName?.value).toBe('posts');
1144
+ expect(envs.resource?.properties?.filterByTk?.value).toBeUndefined();
1145
+ expect(envs.record?.getVar).toBe('ctx.record');
1146
+ expect(envs.record?.value).toBeUndefined();
1147
+ expect(envs.block?.properties?.uid?.getVar).toBe('ctx.blockModel.uid');
1148
+ expect(envs.popup?.properties?.uid?.value).toBe('p1');
1149
+ });
1150
+
1151
+ it('should include block/flowModel/resource envs when blockModel exists', async () => {
1152
+ const ctx = new FlowContext();
1153
+ ctx.defineProperty('model', {
1154
+ value: {
1155
+ uid: 'm1',
1156
+ constructor: { name: 'MyFlowModel', meta: { label: 'MyModel' } },
1157
+ },
1158
+ });
1159
+ ctx.defineProperty('blockModel', {
1160
+ value: {
1161
+ uid: 'b1',
1162
+ constructor: { name: 'TableBlockModel', meta: { label: 'MyBlock' } },
1163
+ resource: {
1164
+ collectionName: 'posts',
1165
+ dataSourceKey: 'main',
1166
+ associationName: 'comments',
1167
+ filterByTk: 1,
1168
+ sourceId: 2,
1169
+ },
1170
+ },
1171
+ });
1172
+
1173
+ const envs = await ctx.getEnvInfos();
1174
+
1175
+ expect(envs.block?.getVar).toBe('ctx.blockModel');
1176
+ expect(envs.block?.properties?.modelClass?.value).toBe('TableBlockModel');
1177
+ expect(envs.flowModel?.getVar).toBe('ctx.model');
1178
+ expect(envs.flowModel?.properties?.label?.value).toBe('MyModel');
1179
+ expect(envs.flowModel?.properties?.modelClass?.value).toBe('MyFlowModel');
1180
+ expect(envs.resource?.getVar).toBe('ctx.blockModel.resource');
1181
+ expect(envs.resource?.properties?.collectionName?.value).toBe('posts');
1182
+ expect(envs.resource?.properties?.filterByTk?.value).toBeUndefined();
1183
+ expect(envs.record?.getVar).toBe('ctx.record');
1184
+ });
1185
+
1186
+ it('should omit popup env section when popup is not available', async () => {
1187
+ const ctx = new FlowContext();
1188
+ const envs = await ctx.getEnvInfos();
1189
+ expect(envs.popup).toBeUndefined();
1190
+ });
1191
+
1192
+ it('should omit optional resource fields and record env when missing', async () => {
1193
+ const ctx = new FlowContext();
1194
+ ctx.defineProperty('popup', {
1195
+ value: Promise.resolve({
1196
+ uid: 'p1',
1197
+ resource: {
1198
+ dataSourceKey: 'main',
1199
+ collectionName: 'posts',
1200
+ // filterByTk/sourceId intentionally omitted
1201
+ },
1202
+ }),
1203
+ });
1204
+
1205
+ const envs = await ctx.getEnvInfos();
1206
+
1207
+ expect(envs.record).toBeUndefined();
1208
+ expect(envs.resource?.properties?.filterByTk).toBeUndefined();
1209
+ expect(envs.resource?.properties?.sourceId).toBeUndefined();
1210
+ expect(envs.popup?.properties?.resource?.properties?.filterByTk).toBeUndefined();
1211
+ expect(envs.popup?.properties?.resource?.properties?.sourceId).toBeUndefined();
1212
+ });
1213
+
1214
+ it('should return currentViewBlocks for page view (BlockModel only)', async () => {
1215
+ const engine = new FlowEngine();
1216
+
1217
+ class ViewModel extends FlowModel {}
1218
+ class BlockModelLike extends FlowModel {
1219
+ onInit(options: any): void {
1220
+ super.onInit(options);
1221
+ this.context.defineProperty('blockModel', { value: this });
1222
+ }
1223
+ }
1224
+ class NonBlockModel extends FlowModel {}
1225
+
1226
+ BlockModelLike.define({ label: 'MyBlock' } as any);
1227
+ NonBlockModel.define({ label: 'NotBlock' } as any);
1228
+
1229
+ engine.registerModels({ ViewModel, BlockModelLike, NonBlockModel });
1230
+
1231
+ engine.createModel({
1232
+ uid: 'view1',
1233
+ use: 'ViewModel',
1234
+ subModels: {
1235
+ blocks: [
1236
+ { uid: 'b1', use: 'BlockModelLike' },
1237
+ { uid: 'b2', use: 'BlockModelLike' },
1238
+ ],
1239
+ other: { uid: 'x1', use: 'NonBlockModel' },
1240
+ },
1241
+ });
1242
+
1243
+ (engine.getModel('b1') as any).resource = { dataSourceKey: 'main', collectionName: 'posts' };
1244
+ (engine.getModel('b2') as any).resource = {
1245
+ dataSourceKey: 'main',
1246
+ collectionName: 'users',
1247
+ associationName: 'roles',
1248
+ };
1249
+
1250
+ engine.context.defineProperty('view', { value: { inputArgs: { viewUid: 'view1' } } });
1251
+
1252
+ const envs = await engine.context.getEnvInfos();
1253
+
1254
+ const blocks = (envs.currentViewBlocks?.value || []) as any[];
1255
+ const uids = blocks.map((b) => b.uid);
1256
+
1257
+ expect(envs.currentViewBlocks?.getVar).toBeUndefined();
1258
+ expect(uids).toEqual(expect.arrayContaining(['b1', 'b2']));
1259
+ expect(uids).not.toContain('x1');
1260
+
1261
+ const b1 = blocks.find((b) => b.uid === 'b1');
1262
+ expect(b1?.label).toBeTruthy();
1263
+ expect(b1).not.toHaveProperty('use');
1264
+ expect(b1?.modelClass).toBe('BlockModelLike');
1265
+ expect(b1?.resource?.collectionName).toBe('posts');
1266
+ });
1267
+
1268
+ it('should prefer current popup view for currentViewBlocks', async () => {
1269
+ const engine = new FlowEngine();
1270
+
1271
+ class ViewModel extends FlowModel {}
1272
+ class BlockModelLike extends FlowModel {
1273
+ onInit(options: any): void {
1274
+ super.onInit(options);
1275
+ this.context.defineProperty('blockModel', { value: this });
1276
+ }
1277
+ }
1278
+
1279
+ BlockModelLike.define({ label: 'MyBlock' } as any);
1280
+ engine.registerModels({ ViewModel, BlockModelLike });
1281
+
1282
+ engine.createModel({
1283
+ uid: 'page1',
1284
+ use: 'ViewModel',
1285
+ subModels: { blocks: [{ uid: 'pageBlock1', use: 'BlockModelLike' }] },
1286
+ });
1287
+ engine.createModel({
1288
+ uid: 'popup1',
1289
+ use: 'ViewModel',
1290
+ subModels: { blocks: [{ uid: 'popupBlock1', use: 'BlockModelLike' }] },
1291
+ });
1292
+
1293
+ engine.context.defineProperty('view', { value: { inputArgs: { viewUid: 'page1' } } });
1294
+ engine.context.defineProperty('popup', { value: { uid: 'popup1' } });
1295
+
1296
+ const envs = await engine.context.getEnvInfos();
1297
+ const blocks = (envs.currentViewBlocks?.value || []) as any[];
1298
+ const uids = blocks.map((b) => b.uid);
1299
+
1300
+ expect(uids).toContain('popupBlock1');
1301
+ expect(uids).not.toContain('pageBlock1');
1302
+ });
1303
+
1304
+ it('should include hidden blocks in currentViewBlocks', async () => {
1305
+ const engine = new FlowEngine();
1306
+
1307
+ class ViewModel extends FlowModel {}
1308
+ class BlockModelLike extends FlowModel {
1309
+ onInit(options: any): void {
1310
+ super.onInit(options);
1311
+ this.context.defineProperty('blockModel', { value: this });
1312
+ }
1313
+ }
1314
+
1315
+ BlockModelLike.define({ label: 'MyBlock' } as any);
1316
+ engine.registerModels({ ViewModel, BlockModelLike });
1317
+
1318
+ engine.createModel({
1319
+ uid: 'view1',
1320
+ use: 'ViewModel',
1321
+ subModels: { blocks: [{ uid: 'b1', use: 'BlockModelLike' }] },
1322
+ });
1323
+
1324
+ const b1 = engine.getModel('b1') as any;
1325
+ b1.hidden = true;
1326
+
1327
+ engine.context.defineProperty('view', { value: { inputArgs: { viewUid: 'view1' } } });
1328
+
1329
+ const envs = await engine.context.getEnvInfos();
1330
+ const blocks = (envs.currentViewBlocks?.value || []) as any[];
1331
+ expect(blocks.map((b) => b.uid)).toContain('b1');
1332
+ });
1333
+
1334
+ it('should omit currentViewBlocks when view is not available', async () => {
1335
+ const engine = new FlowEngine();
1336
+ const envs = await engine.context.getEnvInfos();
1337
+ expect(envs.currentViewBlocks).toBeUndefined();
1338
+ });
1339
+ });
1340
+
755
1341
  describe('FlowEngine context', () => {
756
1342
  it('should support defineProperty on FlowEngine.context', () => {
757
1343
  const engine = new FlowEngine();
@@ -759,6 +1345,96 @@ describe('FlowEngine context', () => {
759
1345
  expect(engine.context.appName).toBe('NocoBase');
760
1346
  });
761
1347
 
1348
+ it('ctx.sql should resolve template variables from caller context in delegate chain', async () => {
1349
+ const engine = new FlowEngine();
1350
+ const request = vi.fn(async () => ({ data: { data: [] } }));
1351
+ engine.context.defineProperty('api', {
1352
+ value: { request },
1353
+ });
1354
+ engine.context.defineProperty('minId', {
1355
+ get: () => 999,
1356
+ cache: false,
1357
+ });
1358
+
1359
+ const callerCtx = new FlowContext();
1360
+ callerCtx.addDelegate(engine.context);
1361
+ callerCtx.defineProperty('minId', {
1362
+ get: () => 1,
1363
+ cache: false,
1364
+ });
1365
+
1366
+ await callerCtx.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
1367
+
1368
+ expect(request).toHaveBeenCalledTimes(1);
1369
+ const config = request.mock.calls[0]?.[0];
1370
+ expect(config?.url).toBe('flowSql:run');
1371
+ expect(config?.data.bind.__var1).toBe(1);
1372
+ });
1373
+
1374
+ it('ctx.sql should not share repository instance between different caller contexts', async () => {
1375
+ const engine = new FlowEngine();
1376
+ const request = vi.fn(async () => ({ data: { data: [] } }));
1377
+ engine.context.defineProperty('api', {
1378
+ value: { request },
1379
+ });
1380
+
1381
+ const caller1 = new FlowContext();
1382
+ caller1.addDelegate(engine.context);
1383
+ caller1.defineProperty('minId', {
1384
+ get: () => 1,
1385
+ cache: false,
1386
+ });
1387
+
1388
+ const caller2 = new FlowContext();
1389
+ caller2.addDelegate(engine.context);
1390
+ caller2.defineProperty('minId', {
1391
+ get: () => 2,
1392
+ cache: false,
1393
+ });
1394
+
1395
+ expect(caller1.sql).not.toBe(caller2.sql);
1396
+
1397
+ await caller1.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
1398
+ await caller2.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
1399
+
1400
+ expect(request).toHaveBeenCalledTimes(2);
1401
+ const config1 = request.mock.calls[0]?.[0];
1402
+ const config2 = request.mock.calls[1]?.[0];
1403
+ expect(config1?.data.bind.__var1).toBe(1);
1404
+ expect(config2?.data.bind.__var1).toBe(2);
1405
+ });
1406
+
1407
+ it('engine.context.sql should keep working when accessed directly', async () => {
1408
+ const engine = new FlowEngine();
1409
+ const request = vi.fn(async () => ({ data: { data: [] } }));
1410
+ engine.context.defineProperty('api', {
1411
+ value: { request },
1412
+ });
1413
+ engine.context.defineProperty('minId', {
1414
+ get: () => 3,
1415
+ cache: false,
1416
+ });
1417
+
1418
+ await engine.context.sql.run('SELECT * FROM users WHERE id > {{ctx.minId}}', { type: 'selectRows' });
1419
+
1420
+ expect(request).toHaveBeenCalledTimes(1);
1421
+ const config = request.mock.calls[0]?.[0];
1422
+ expect(config?.url).toBe('flowSql:run');
1423
+ expect(config?.data.bind.__var1).toBe(3);
1424
+ });
1425
+
1426
+ it('engine.context.getVar should resolve variable by path', async () => {
1427
+ const engine = new FlowEngine();
1428
+ engine.context.defineProperty('foo', { value: { bar: 1 } });
1429
+
1430
+ const v1 = await (engine.context as any).getVar('ctx.foo.bar');
1431
+ expect(v1).toBe(1);
1432
+
1433
+ await expect((engine.context as any).getVar('{{ ctx.foo.bar }}')).rejects.toThrow();
1434
+
1435
+ await expect((engine.context as any).getVar('foo.bar')).rejects.toThrow();
1436
+ });
1437
+
762
1438
  it('engine.context.runAction should resolve action from engine.getAction', async () => {
763
1439
  const engine = new FlowEngine();
764
1440
 
@@ -1132,6 +1808,22 @@ describe('getPropertyMetaTree with deep delegate meta', () => {
1132
1808
  });
1133
1809
 
1134
1810
  describe('FlowContext resolveOnServer selective server resolution', () => {
1811
+ it('resolves ctx.date expressions on client context', async () => {
1812
+ const engine = new FlowEngine();
1813
+ const out = await (engine.context as any).resolveJsonTemplate({
1814
+ today: '{{ ctx.date.preset.today }}',
1815
+ next12: '{{ ctx.date.relative.next.day.n12 }}',
1816
+ now: '{{ ctx.date.preset.now }}',
1817
+ });
1818
+
1819
+ expect(typeof out.today).toBe('string');
1820
+ expect(out.today).toMatch(/^\d{4}-\d{2}-\d{2}$/);
1821
+ expect(typeof out.next12).toBe('string');
1822
+ expect(out.next12).toMatch(/^\d{4}-\d{2}-\d{2}$/);
1823
+ expect(typeof out.now).toBe('string');
1824
+ expect(out.now.length).toBeGreaterThan(0);
1825
+ });
1826
+
1135
1827
  it('does not call server by default (no resolveOnServer set)', async () => {
1136
1828
  const engine = new FlowEngine();
1137
1829
  const api = { request: vi.fn() } as any;