@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.
- package/lib/BlockScopedFlowEngine.js +0 -1
- package/lib/FlowDefinition.d.ts +2 -0
- package/lib/JSRunner.d.ts +6 -0
- package/lib/JSRunner.js +32 -2
- package/lib/ViewScopedFlowEngine.js +3 -0
- package/lib/acl/Acl.js +13 -3
- package/lib/components/FlowContextSelector.js +155 -10
- package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
- package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
- package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
- package/lib/components/variables/VariableInput.js +9 -4
- package/lib/components/variables/VariableTag.js +46 -39
- package/lib/components/variables/utils.d.ts +7 -0
- package/lib/components/variables/utils.js +42 -2
- package/lib/data-source/index.d.ts +7 -27
- package/lib/data-source/index.js +81 -51
- package/lib/executor/FlowExecutor.d.ts +2 -1
- package/lib/executor/FlowExecutor.js +163 -22
- package/lib/flowContext.d.ts +230 -7
- package/lib/flowContext.js +2267 -148
- package/lib/flowEngine.d.ts +21 -0
- package/lib/flowEngine.js +56 -8
- package/lib/flowI18n.js +6 -4
- package/lib/flowSettings.js +17 -11
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -0
- package/lib/locale/en-US.json +9 -2
- package/lib/locale/index.d.ts +14 -0
- package/lib/locale/zh-CN.json +8 -1
- package/lib/models/CollectionFieldModel.d.ts +1 -0
- package/lib/models/CollectionFieldModel.js +3 -2
- package/lib/models/flowModel.js +12 -1
- package/lib/provider.js +5 -5
- package/lib/resources/baseRecordResource.d.ts +5 -0
- package/lib/resources/baseRecordResource.js +24 -0
- package/lib/resources/multiRecordResource.d.ts +1 -0
- package/lib/resources/multiRecordResource.js +11 -4
- package/lib/resources/singleRecordResource.js +2 -0
- package/lib/resources/sqlResource.d.ts +4 -3
- package/lib/resources/sqlResource.js +8 -3
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
- package/lib/runjs-context/contexts/base.js +706 -41
- package/lib/runjs-context/contributions.d.ts +33 -0
- package/lib/runjs-context/contributions.js +88 -0
- package/lib/runjs-context/helpers.js +12 -1
- package/lib/runjs-context/setup.js +6 -0
- package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
- package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
- package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
- package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
- package/lib/runjs-context/snippets/index.d.ts +11 -1
- package/lib/runjs-context/snippets/index.js +61 -40
- package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
- package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
- package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
- package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
- package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
- package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
- package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
- package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
- package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
- package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
- package/lib/runjsLibs.d.ts +28 -0
- package/lib/runjsLibs.js +532 -0
- package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
- package/lib/scheduler/ModelOperationScheduler.js +25 -21
- package/lib/types.d.ts +27 -0
- package/lib/utils/associationObjectVariable.d.ts +2 -2
- package/lib/utils/createCollectionContextMeta.js +1 -0
- package/lib/utils/createEphemeralContext.js +2 -2
- package/lib/utils/dateVariable.d.ts +16 -0
- package/lib/utils/dateVariable.js +380 -0
- package/lib/utils/exceptions.d.ts +7 -0
- package/lib/utils/exceptions.js +10 -0
- package/lib/utils/index.d.ts +8 -3
- package/lib/utils/index.js +45 -0
- package/lib/utils/params-resolvers.js +16 -9
- package/lib/utils/resolveModuleUrl.d.ts +58 -0
- package/lib/utils/resolveModuleUrl.js +65 -0
- package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
- package/lib/utils/resolveRunJSObjectValues.js +61 -0
- package/lib/utils/runjsModuleLoader.d.ts +58 -0
- package/lib/utils/runjsModuleLoader.js +422 -0
- package/lib/utils/runjsTemplateCompat.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +743 -0
- package/lib/utils/runjsValue.d.ts +29 -0
- package/lib/utils/runjsValue.js +275 -0
- package/lib/utils/safeGlobals.d.ts +18 -8
- package/lib/utils/safeGlobals.js +164 -17
- package/lib/utils/schema-utils.d.ts +10 -0
- package/lib/utils/schema-utils.js +61 -0
- package/lib/views/createViewMeta.d.ts +0 -7
- package/lib/views/createViewMeta.js +19 -70
- package/lib/views/index.d.ts +1 -2
- package/lib/views/index.js +4 -3
- package/lib/views/useDialog.js +7 -2
- package/lib/views/useDrawer.js +7 -2
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +43 -6
- package/lib/views/usePopover.js +4 -1
- package/lib/views/viewEvents.d.ts +17 -0
- package/lib/views/viewEvents.js +90 -0
- package/package.json +4 -4
- package/src/BlockScopedFlowEngine.ts +2 -5
- package/src/JSRunner.ts +44 -2
- package/src/ViewScopedFlowEngine.ts +4 -0
- package/src/__tests__/JSRunner.test.ts +64 -0
- package/src/__tests__/createViewMeta.popup.test.ts +62 -1
- package/src/__tests__/flowContext.test.ts +693 -1
- package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
- package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
- package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
- package/src/__tests__/flowRuntimeContext.test.ts +2 -1
- package/src/__tests__/flowSettings.open.test.tsx +123 -19
- package/src/__tests__/runjsContext.test.ts +10 -7
- package/src/__tests__/runjsContextImplementations.test.ts +34 -3
- package/src/__tests__/runjsContextRuntime.test.ts +3 -3
- package/src/__tests__/runjsContributions.test.ts +89 -0
- package/src/__tests__/runjsExternalLibs.test.ts +242 -0
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/__tests__/runjsLocales.test.ts +4 -1
- package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
- package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
- package/src/__tests__/runjsSnippets.test.ts +40 -3
- package/src/acl/Acl.tsx +3 -3
- package/src/components/FlowContextSelector.tsx +208 -12
- package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
- package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
- package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
- package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
- package/src/components/variables/VariableInput.tsx +12 -4
- package/src/components/variables/VariableTag.tsx +54 -45
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
- package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
- package/src/components/variables/__tests__/utils.test.ts +81 -3
- package/src/components/variables/utils.ts +67 -6
- package/src/data-source/index.ts +85 -110
- package/src/executor/FlowExecutor.ts +200 -23
- package/src/executor/__tests__/flowExecutor.test.ts +66 -0
- package/src/flowContext.ts +2986 -211
- package/src/flowEngine.ts +59 -8
- package/src/flowI18n.ts +7 -5
- package/src/flowSettings.ts +18 -12
- package/src/index.ts +14 -1
- package/src/locale/en-US.json +9 -2
- package/src/locale/zh-CN.json +8 -1
- package/src/models/CollectionFieldModel.tsx +3 -1
- package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
- package/src/models/__tests__/flowModel.test.ts +20 -4
- package/src/models/flowModel.tsx +13 -1
- package/src/provider.tsx +7 -6
- package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
- package/src/resources/__tests__/sqlResource.test.ts +60 -0
- package/src/resources/baseRecordResource.ts +31 -0
- package/src/resources/multiRecordResource.ts +11 -4
- package/src/resources/singleRecordResource.ts +3 -0
- package/src/resources/sqlResource.ts +11 -6
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
- package/src/runjs-context/contexts/base.ts +715 -44
- package/src/runjs-context/contributions.ts +88 -0
- package/src/runjs-context/helpers.ts +11 -1
- package/src/runjs-context/setup.ts +6 -0
- package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
- package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
- package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
- package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
- package/src/runjs-context/snippets/index.ts +75 -41
- package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
- package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
- package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
- package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
- package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
- package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
- package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
- package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
- package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
- package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
- package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
- package/src/runjsLibs.ts +622 -0
- package/src/scheduler/ModelOperationScheduler.ts +27 -21
- package/src/types.ts +38 -1
- package/src/utils/__tests__/dateVariable.test.ts +101 -0
- package/src/utils/__tests__/params-resolvers.test.ts +40 -0
- package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
- package/src/utils/__tests__/runjsValue.test.ts +44 -0
- package/src/utils/__tests__/safeGlobals.test.ts +57 -2
- package/src/utils/__tests__/utils.test.ts +95 -0
- package/src/utils/associationObjectVariable.ts +2 -2
- package/src/utils/createCollectionContextMeta.ts +1 -0
- package/src/utils/createEphemeralContext.ts +5 -4
- package/src/utils/dateVariable.ts +397 -0
- package/src/utils/exceptions.ts +11 -0
- package/src/utils/index.ts +37 -3
- package/src/utils/params-resolvers.ts +23 -9
- package/src/utils/resolveModuleUrl.ts +91 -0
- package/src/utils/resolveRunJSObjectValues.ts +46 -0
- package/src/utils/runjsModuleLoader.ts +553 -0
- package/src/utils/runjsTemplateCompat.ts +828 -0
- package/src/utils/runjsValue.ts +287 -0
- package/src/utils/safeGlobals.ts +188 -17
- package/src/utils/schema-utils.ts +79 -0
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
- package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
- package/src/views/createViewMeta.ts +22 -75
- package/src/views/index.tsx +1 -2
- package/src/views/useDialog.tsx +8 -1
- package/src/views/useDrawer.tsx +8 -1
- package/src/views/usePage.tsx +51 -5
- package/src/views/usePopover.tsx +4 -1
- package/src/views/viewEvents.ts +55 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { uid as genUid } from 'uid/secure';
|
|
11
11
|
import type { FlowEngine } from '../flowEngine';
|
|
12
12
|
import type { FlowModel } from '../models/flowModel';
|
|
13
|
+
import { FlowExitAllException } from '../utils/exceptions';
|
|
13
14
|
|
|
14
15
|
type LifecycleType =
|
|
15
16
|
| 'created'
|
|
@@ -36,6 +37,8 @@ export interface LifecycleEvent {
|
|
|
36
37
|
error?: any;
|
|
37
38
|
inputArgs?: Record<string, any>;
|
|
38
39
|
result?: any;
|
|
40
|
+
flowKey?: string;
|
|
41
|
+
stepKey?: string;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
type ScheduledItem = {
|
|
@@ -162,37 +165,37 @@ export class ModelOperationScheduler {
|
|
|
162
165
|
const emitter = this.engine.emitter;
|
|
163
166
|
if (!emitter || typeof emitter.on !== 'function') return;
|
|
164
167
|
|
|
165
|
-
const onCreated = (e: LifecycleEvent) => {
|
|
166
|
-
this.processLifecycleEvent(e.uid, { ...e, type: 'created' });
|
|
168
|
+
const onCreated = async (e: LifecycleEvent) => {
|
|
169
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: 'created' });
|
|
167
170
|
};
|
|
168
171
|
emitter.on('model:created', onCreated);
|
|
169
172
|
this.unbindHandlers.push(() => emitter.off('model:created', onCreated));
|
|
170
173
|
|
|
171
|
-
const onMounted = (e: LifecycleEvent) => {
|
|
172
|
-
this.processLifecycleEvent(e.uid, { ...e, type: 'mounted' });
|
|
174
|
+
const onMounted = async (e: LifecycleEvent) => {
|
|
175
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: 'mounted' });
|
|
173
176
|
};
|
|
174
177
|
emitter.on('model:mounted', onMounted);
|
|
175
178
|
this.unbindHandlers.push(() => emitter.off('model:mounted', onMounted));
|
|
176
179
|
|
|
177
|
-
const onGenericBeforeStart = (e: LifecycleEvent) => {
|
|
178
|
-
this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:start' });
|
|
180
|
+
const onGenericBeforeStart = async (e: LifecycleEvent) => {
|
|
181
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:start' });
|
|
179
182
|
};
|
|
180
183
|
emitter.on('model:event:beforeRender:start', onGenericBeforeStart);
|
|
181
184
|
this.unbindHandlers.push(() => emitter.off('model:event:beforeRender:start', onGenericBeforeStart));
|
|
182
185
|
|
|
183
|
-
const onGenericBeforeEnd = (e: LifecycleEvent) => {
|
|
184
|
-
this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:end' });
|
|
186
|
+
const onGenericBeforeEnd = async (e: LifecycleEvent) => {
|
|
187
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:end' });
|
|
185
188
|
};
|
|
186
189
|
emitter.on('model:event:beforeRender:end', onGenericBeforeEnd);
|
|
187
190
|
this.unbindHandlers.push(() => emitter.off('model:event:beforeRender:end', onGenericBeforeEnd));
|
|
188
191
|
|
|
189
|
-
const onUnmounted = (e: LifecycleEvent) => {
|
|
190
|
-
this.processLifecycleEvent(e.uid, { ...e, type: 'unmounted' });
|
|
192
|
+
const onUnmounted = async (e: LifecycleEvent) => {
|
|
193
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: 'unmounted' });
|
|
191
194
|
};
|
|
192
195
|
emitter.on('model:unmounted', onUnmounted);
|
|
193
196
|
this.unbindHandlers.push(() => emitter.off('model:unmounted', onUnmounted));
|
|
194
197
|
|
|
195
|
-
const onDestroyed = (e: LifecycleEvent) => {
|
|
198
|
+
const onDestroyed = async (e: LifecycleEvent) => {
|
|
196
199
|
const targetBucket = this.itemsByTargetUid.get(e.uid);
|
|
197
200
|
const event = { ...e, type: 'destroyed' as const };
|
|
198
201
|
if (targetBucket && targetBucket.size) {
|
|
@@ -201,7 +204,7 @@ export class ModelOperationScheduler {
|
|
|
201
204
|
const it = this.itemsById.get(id);
|
|
202
205
|
if (!it) continue;
|
|
203
206
|
if (this.shouldTrigger(it.options.when, event)) {
|
|
204
|
-
|
|
207
|
+
await this.tryExecuteOnce(id, event);
|
|
205
208
|
} else {
|
|
206
209
|
this.internalCancel(id);
|
|
207
210
|
}
|
|
@@ -220,14 +223,14 @@ export class ModelOperationScheduler {
|
|
|
220
223
|
if (this.subscribedEventNames.has(name)) return;
|
|
221
224
|
this.subscribedEventNames.add(name);
|
|
222
225
|
const emitter = this.engine.emitter;
|
|
223
|
-
const onStart = (e: LifecycleEvent) => {
|
|
224
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` as
|
|
226
|
+
const onStart = async (e: LifecycleEvent) => {
|
|
227
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` as LifecycleType });
|
|
225
228
|
};
|
|
226
|
-
const onEnd = (e: LifecycleEvent) => {
|
|
227
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` as
|
|
229
|
+
const onEnd = async (e: LifecycleEvent) => {
|
|
230
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` as LifecycleType });
|
|
228
231
|
};
|
|
229
|
-
const onError = (e: LifecycleEvent) => {
|
|
230
|
-
this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` as
|
|
232
|
+
const onError = async (e: LifecycleEvent) => {
|
|
233
|
+
await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` as LifecycleType });
|
|
231
234
|
};
|
|
232
235
|
emitter.on(`model:event:${name}:start`, onStart);
|
|
233
236
|
emitter.on(`model:event:${name}:end`, onEnd);
|
|
@@ -239,12 +242,12 @@ export class ModelOperationScheduler {
|
|
|
239
242
|
|
|
240
243
|
private parseEventWhen(when?: ScheduleWhen): { name: string; phase: 'start' | 'end' | 'error' } | null {
|
|
241
244
|
if (!when || typeof when !== 'string') return null;
|
|
242
|
-
const m = /^event:(
|
|
245
|
+
const m = /^event:(.+):(start|end|error)$/.exec(when);
|
|
243
246
|
if (!m) return null;
|
|
244
247
|
return { name: m[1], phase: m[2] as 'start' | 'end' | 'error' };
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
private processLifecycleEvent(targetUid: string, event: LifecycleEvent) {
|
|
250
|
+
private async processLifecycleEvent(targetUid: string, event: LifecycleEvent) {
|
|
248
251
|
const targetBucket = this.itemsByTargetUid.get(targetUid);
|
|
249
252
|
if (!targetBucket || targetBucket.size === 0) return;
|
|
250
253
|
const ids = Array.from(targetBucket.keys());
|
|
@@ -253,7 +256,7 @@ export class ModelOperationScheduler {
|
|
|
253
256
|
if (!item) continue;
|
|
254
257
|
const should = this.shouldTrigger(item.options.when, event);
|
|
255
258
|
if (!should) continue;
|
|
256
|
-
|
|
259
|
+
await this.tryExecuteOnce(id, event);
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
|
|
@@ -271,6 +274,9 @@ export class ModelOperationScheduler {
|
|
|
271
274
|
if (!model) return;
|
|
272
275
|
await Promise.resolve(item.fn(model));
|
|
273
276
|
} catch (err) {
|
|
277
|
+
if (err instanceof FlowExitAllException) {
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
274
280
|
this.engine.logger?.error?.(
|
|
275
281
|
{ err, id, fromUid: item.fromUid, toUid: item.toUid, when: item.options.when },
|
|
276
282
|
'ModelOperationScheduler: operation execution failed',
|
package/src/types.ts
CHANGED
|
@@ -172,6 +172,18 @@ export interface ActionDefinition<TModel extends FlowModel = FlowModel, TCtx ext
|
|
|
172
172
|
* - StepDefinition.hideInSettings can override the ActionDefinition value.
|
|
173
173
|
*/
|
|
174
174
|
hideInSettings?: boolean | ((ctx: TCtx) => boolean | Promise<boolean>);
|
|
175
|
+
/**
|
|
176
|
+
* Whether to disable this step/action in settings menus.
|
|
177
|
+
* - Supports static boolean and dynamic decision based on runtime context.
|
|
178
|
+
* - StepDefinition.disabledInSettings can override the ActionDefinition value.
|
|
179
|
+
*/
|
|
180
|
+
disabledInSettings?: boolean | ((ctx: TCtx) => boolean | Promise<boolean>);
|
|
181
|
+
/**
|
|
182
|
+
* Optional reason shown when this step/action is disabled in settings menus.
|
|
183
|
+
* - Supports static string and dynamic resolver based on runtime context.
|
|
184
|
+
* - StepDefinition.disabledReasonInSettings can override the ActionDefinition value.
|
|
185
|
+
*/
|
|
186
|
+
disabledReasonInSettings?: string | ((ctx: TCtx) => string | Promise<string>);
|
|
175
187
|
/**
|
|
176
188
|
* 在执行 Action 前为 ctx 定义临时属性。
|
|
177
189
|
* - 仅支持 PropertyOptions 形态(例如:{ foo: { value: 5 } });
|
|
@@ -213,12 +225,37 @@ export type FlowEventName =
|
|
|
213
225
|
// fallback to any string for extensibility
|
|
214
226
|
| (string & {});
|
|
215
227
|
|
|
228
|
+
/**
|
|
229
|
+
* 事件流的执行时机(phase)。
|
|
230
|
+
*
|
|
231
|
+
* 说明:
|
|
232
|
+
* - 缺省(phase 未配置)表示保持现有行为;
|
|
233
|
+
* - 当配置了 phase 时,运行时会将其映射为 `scheduleModelOperation` 的 `when` 锚点;
|
|
234
|
+
* - phase 同时适用于动态事件流(实例级)与静态流(内置)。
|
|
235
|
+
*/
|
|
236
|
+
export type FlowEventPhase =
|
|
237
|
+
| 'beforeAllFlows'
|
|
238
|
+
| 'afterAllFlows'
|
|
239
|
+
| 'beforeFlow'
|
|
240
|
+
| 'afterFlow'
|
|
241
|
+
| 'beforeStep'
|
|
242
|
+
| 'afterStep';
|
|
243
|
+
|
|
216
244
|
/**
|
|
217
245
|
* Flow 事件类型(供 FlowDefinitionOptions.on 使用)。
|
|
218
246
|
*/
|
|
219
247
|
export type FlowEvent<TModel extends FlowModel = FlowModel> =
|
|
220
248
|
| FlowEventName
|
|
221
|
-
| {
|
|
249
|
+
| {
|
|
250
|
+
eventName: FlowEventName;
|
|
251
|
+
defaultParams?: Record<string, any>;
|
|
252
|
+
/** 动态事件流的执行时机(默认 beforeAllFlows) */
|
|
253
|
+
phase?: FlowEventPhase;
|
|
254
|
+
/** phase 为 beforeFlow/afterFlow/beforeStep/afterStep 时使用 */
|
|
255
|
+
flowKey?: string;
|
|
256
|
+
/** phase 为 beforeStep/afterStep 时使用 */
|
|
257
|
+
stepKey?: string;
|
|
258
|
+
};
|
|
222
259
|
|
|
223
260
|
/**
|
|
224
261
|
* 事件分发选项。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
decodeBase64Url,
|
|
13
|
+
encodeBase64Url,
|
|
14
|
+
isCompleteCtxDatePath,
|
|
15
|
+
isCtxDateExpression,
|
|
16
|
+
parseCtxDateExpression,
|
|
17
|
+
resolveCtxDatePath,
|
|
18
|
+
serializeCtxDateValue,
|
|
19
|
+
} from '../dateVariable';
|
|
20
|
+
|
|
21
|
+
describe('dateVariable utils', () => {
|
|
22
|
+
it('encodes and decodes base64url', () => {
|
|
23
|
+
const raw = '2026-02-12 10:11:12';
|
|
24
|
+
const encoded = encodeBase64Url(raw);
|
|
25
|
+
const decoded = decodeBase64Url(encoded);
|
|
26
|
+
expect(decoded).toBe(raw);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('detects ctx.date expression', () => {
|
|
30
|
+
expect(isCtxDateExpression('{{ ctx.date.preset.today }}')).toBe(true);
|
|
31
|
+
expect(isCtxDateExpression('{{ ctx.user.name }}')).toBe(false);
|
|
32
|
+
expect(isCtxDateExpression('')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('serializes preset/relative/exact single/range', () => {
|
|
36
|
+
expect(serializeCtxDateValue({ type: 'today' })).toBe('{{ ctx.date.preset.today }}');
|
|
37
|
+
expect(serializeCtxDateValue({ type: 'next', unit: 'day', number: 12 })).toBe(
|
|
38
|
+
'{{ ctx.date.relative.next.day.n12 }}',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const single = serializeCtxDateValue('2026-02-12');
|
|
42
|
+
expect(single?.startsWith('{{ ctx.date.exact.single.date.v')).toBe(true);
|
|
43
|
+
|
|
44
|
+
const range = serializeCtxDateValue(['2026-02-12', '2026-02-20']);
|
|
45
|
+
expect(range?.startsWith('{{ ctx.date.exact.range.date.v')).toBe(true);
|
|
46
|
+
expect(range?.includes('.v')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('parses expression back to ui value', () => {
|
|
50
|
+
expect(parseCtxDateExpression('{{ ctx.date.preset.today }}')).toEqual({ type: 'today' });
|
|
51
|
+
expect(parseCtxDateExpression('{{ ctx.date.relative.past.month.n2 }}')).toEqual({
|
|
52
|
+
type: 'past',
|
|
53
|
+
unit: 'month',
|
|
54
|
+
number: 2,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const singleExpr = serializeCtxDateValue('2026-02-12')!;
|
|
58
|
+
expect(parseCtxDateExpression(singleExpr)).toBe('2026-02-12');
|
|
59
|
+
|
|
60
|
+
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])!;
|
|
61
|
+
expect(parseCtxDateExpression(rangeExpr)).toEqual(['2026-02-12', '2026-02-20']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resolves preset/relative/exact path', () => {
|
|
65
|
+
expect(typeof resolveCtxDatePath(['date', 'preset', 'now'])).toBe('string');
|
|
66
|
+
|
|
67
|
+
const today = resolveCtxDatePath(['date', 'preset', 'today']);
|
|
68
|
+
expect(typeof today).toBe('string');
|
|
69
|
+
expect(today).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
70
|
+
|
|
71
|
+
const rel = resolveCtxDatePath(['date', 'relative', 'next', 'day', 'n12']);
|
|
72
|
+
expect(typeof rel).toBe('string');
|
|
73
|
+
expect(rel).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
74
|
+
|
|
75
|
+
const singleExpr = serializeCtxDateValue('2026-02-12')!;
|
|
76
|
+
const token = singleExpr.replace('{{ ctx.date.exact.single.date.', '').replace(' }}', '');
|
|
77
|
+
expect(resolveCtxDatePath(['date', 'exact', 'single', 'date', token])).toBe('2026-02-12');
|
|
78
|
+
|
|
79
|
+
const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])!;
|
|
80
|
+
const parts = rangeExpr.replace('{{ ctx.date.exact.range.date.', '').replace(' }}', '').split('.');
|
|
81
|
+
expect(resolveCtxDatePath(['date', 'exact', 'range', 'date', parts[0], parts[1]])).toEqual([
|
|
82
|
+
'2026-02-12',
|
|
83
|
+
'2026-02-20',
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('validates complete ctx.date path', () => {
|
|
88
|
+
expect(isCompleteCtxDatePath(['date', 'preset', 'today'])).toBe(true);
|
|
89
|
+
expect(isCompleteCtxDatePath(['date', 'relative', 'next', 'day', 'n12'])).toBe(true);
|
|
90
|
+
expect(isCompleteCtxDatePath(['date', 'exact', 'single', 'date', 'vabc'])).toBe(true);
|
|
91
|
+
expect(isCompleteCtxDatePath(['date', 'exact', 'range', 'date', 'vabc', 'vdef'])).toBe(true);
|
|
92
|
+
expect(isCompleteCtxDatePath(['date', 'relative', 'next', 'day'])).toBe(false);
|
|
93
|
+
expect(isCompleteCtxDatePath(['user', 'name'])).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('handles invalid base64 and path gracefully', () => {
|
|
97
|
+
expect(decodeBase64Url('@@@')).toBeUndefined();
|
|
98
|
+
expect(parseCtxDateExpression('{{ ctx.date.exact.single.date.v@@@ }}')).toBeUndefined();
|
|
99
|
+
expect(resolveCtxDatePath(['date', 'exact', 'single', 'date', 'v@@@'])).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -491,6 +491,46 @@ describe('resolveExpressions', () => {
|
|
|
491
491
|
],
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
|
+
|
|
495
|
+
test('should resolve dot-only path with dashed keys', async () => {
|
|
496
|
+
ctx.defineProperty('formValues', {
|
|
497
|
+
value: {
|
|
498
|
+
'oho-test': {
|
|
499
|
+
'o2m-users': [1, 2],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const params = '{{ctx.formValues.oho-test.o2m-users}}';
|
|
505
|
+
|
|
506
|
+
const result = await resolveExpressions(params, ctx);
|
|
507
|
+
|
|
508
|
+
expect(result).toEqual([1, 2]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('should resolve dashed keys inside template strings', async () => {
|
|
512
|
+
ctx.defineProperty('formValues', {
|
|
513
|
+
value: {
|
|
514
|
+
'oho-test': {
|
|
515
|
+
'o2m-users': 'X',
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const params = 'prefix {{ctx.formValues.oho-test.o2m-users}} suffix';
|
|
521
|
+
|
|
522
|
+
const result = await resolveExpressions(params, ctx);
|
|
523
|
+
|
|
524
|
+
expect(result).toEqual('prefix X suffix');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('should not treat subtraction as dashed key path', async () => {
|
|
528
|
+
const params = '{{ctx.aa.bb-ctx.cc}}';
|
|
529
|
+
|
|
530
|
+
const result = await resolveExpressions(params, ctx);
|
|
531
|
+
|
|
532
|
+
expect(result).toEqual(5);
|
|
533
|
+
});
|
|
494
534
|
});
|
|
495
535
|
|
|
496
536
|
// 测试高级功能:多表达式模板字符串
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { runjsRequireAsync } from '../runjsModuleLoader';
|
|
12
|
+
import { __resetRunJSSafeGlobalsRegistryForTests, createSafeWindow } from '../safeGlobals';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
__resetRunJSSafeGlobalsRegistryForTests();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('runjsRequireAsync auto whitelist', () => {
|
|
19
|
+
it('should allow safeWindow to access globals introduced during requireAsync', async () => {
|
|
20
|
+
const key = '__nb_require_async_added_global__';
|
|
21
|
+
delete (window as any)[key];
|
|
22
|
+
|
|
23
|
+
const safeWin: any = createSafeWindow();
|
|
24
|
+
expect(() => safeWin[key]).toThrow(/not allowed/);
|
|
25
|
+
|
|
26
|
+
const requirejs: any = (deps: string[], onLoad: (...args: any[]) => void) => {
|
|
27
|
+
// Simulate a remote library attaching itself to the real window.
|
|
28
|
+
(window as any)[key] = { ok: true };
|
|
29
|
+
onLoad(undefined);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
await runjsRequireAsync(requirejs, 'https://example.com/fake-lib.js');
|
|
33
|
+
|
|
34
|
+
expect(safeWin[key]).toEqual({ ok: true });
|
|
35
|
+
|
|
36
|
+
delete (window as any)[key];
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
import * as jsxTransform from '../../utils/jsxTransform';
|
|
12
|
+
import { prepareRunJsCode, preprocessRunJsTemplates } from '../runjsTemplateCompat';
|
|
13
|
+
|
|
14
|
+
describe('runjsTemplateCompat', () => {
|
|
15
|
+
describe('preprocessRunJsTemplates', () => {
|
|
16
|
+
it('hoists bare {{ }} placeholders into top-level resolved vars', () => {
|
|
17
|
+
const src = `const a = {{ctx.user.id}};`;
|
|
18
|
+
const out = preprocessRunJsTemplates(src);
|
|
19
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
|
|
20
|
+
expect(out).toContain(`const a = __runjs_ctx_tpl_0;`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('replaces string literals containing {{ }} via split/join, without injecting await into nested functions', () => {
|
|
24
|
+
const src = `const s = '{{ctx.user.id}}';`;
|
|
25
|
+
const out = preprocessRunJsTemplates(src);
|
|
26
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
|
|
27
|
+
expect(out).toContain(`__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}")`);
|
|
28
|
+
expect(out).toContain(`.split("{{ctx.user.id}}").join(`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not transform arguments inside explicit ctx.resolveJsonTemplate(...) call', () => {
|
|
32
|
+
const src = `const v = await ctx.resolveJsonTemplate('{{ctx.user.id}}');\nconst s = '{{ctx.user.id}}';`;
|
|
33
|
+
const out = preprocessRunJsTemplates(src);
|
|
34
|
+
// inside call: keep raw
|
|
35
|
+
expect(out).toContain(`await ctx.resolveJsonTemplate('{{ctx.user.id}}')`);
|
|
36
|
+
// outside call: transform
|
|
37
|
+
expect(out).toContain(`__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}")`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('keeps template markers in comments unchanged', () => {
|
|
41
|
+
const src = `// {{ctx.user.id}}\nconst a = 1;`;
|
|
42
|
+
const out = preprocessRunJsTemplates(src);
|
|
43
|
+
expect(out).toBe(src);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('supports template literals containing {{ }}', () => {
|
|
47
|
+
const src = 'const s = `hi {{ctx.user.name}}`;';
|
|
48
|
+
const out = preprocessRunJsTemplates(src);
|
|
49
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.name}}");`);
|
|
50
|
+
expect(out).toContain(`\`hi {{ctx.user.name}}\``);
|
|
51
|
+
expect(out).toContain(`.split("{{ctx.user.name}}").join(`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does not rewrite non-ctx {{ }} patterns (e.g. JSX style object) while still rewriting ctx placeholders', () => {
|
|
55
|
+
const src = `const id = {{ctx.user.id}};\nctx.render(<div style={{ width: '100%' }} />);`;
|
|
56
|
+
const out = preprocessRunJsTemplates(src);
|
|
57
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
|
|
58
|
+
expect(out).toContain(`style={{ width: '100%' }}`);
|
|
59
|
+
expect(out).not.toContain(`resolveJsonTemplate("{{ width: '100%' }}")`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('avoids injecting await into non-async nested function bodies', () => {
|
|
63
|
+
const src = `
|
|
64
|
+
function f() {
|
|
65
|
+
return {{ctx.user.id}};
|
|
66
|
+
}
|
|
67
|
+
return f();
|
|
68
|
+
`.trim();
|
|
69
|
+
const out = preprocessRunJsTemplates(src);
|
|
70
|
+
// The only await should be in the top-level preamble
|
|
71
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
|
|
72
|
+
expect(out).toContain(`return __runjs_ctx_tpl_0;`);
|
|
73
|
+
expect(out).not.toContain(`return (await ctx.resolveJsonTemplate`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('is tolerant to already-preprocessed code (idempotent heuristic)', () => {
|
|
77
|
+
const src =
|
|
78
|
+
`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");\n` +
|
|
79
|
+
`const s = ("{{ctx.user.id}}").split("{{ctx.user.id}}").join(__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}"));`;
|
|
80
|
+
const out = preprocessRunJsTemplates(src);
|
|
81
|
+
expect(out).toBe(src);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rewrites object literal string keys via computed property to keep syntax valid', () => {
|
|
85
|
+
const src = `const o = { '{{ctx.user.id}}': 1 };`;
|
|
86
|
+
const out = preprocessRunJsTemplates(src);
|
|
87
|
+
expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
|
|
88
|
+
expect(out).toContain(`{ [`);
|
|
89
|
+
expect(out).toContain(`]: 1 }`);
|
|
90
|
+
expect(out).toContain(`.split("{{ctx.user.id}}").join(`);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('prepareRunJsCode', () => {
|
|
95
|
+
it('preprocesses templates and compiles JSX', async () => {
|
|
96
|
+
const src = `
|
|
97
|
+
const name = '{{ctx.user.name}}';
|
|
98
|
+
ctx.render(<div className="x">{name}</div>);
|
|
99
|
+
`.trim();
|
|
100
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: true });
|
|
101
|
+
expect(out).toMatch(/ctx\.React\.createElement/);
|
|
102
|
+
expect(out).toMatch(/ctx\.resolveJsonTemplate/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('injects ctx.libs ensure preamble for member access', async () => {
|
|
106
|
+
const src = `return ctx.libs.lodash;`;
|
|
107
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
108
|
+
expect(out).toContain(`/* __runjs_ensure_libs */`);
|
|
109
|
+
expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('injects ctx.libs ensure preamble for bracket access with string literal', async () => {
|
|
113
|
+
const src = `return ctx.libs['lodash'];`;
|
|
114
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
115
|
+
expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('injects ctx.libs ensure preamble for object destructuring', async () => {
|
|
119
|
+
const src = `const { lodash } = ctx.libs;\nreturn lodash;`;
|
|
120
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
121
|
+
expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not inject ctx.libs preamble when ctx.libs only appears in string/comment', async () => {
|
|
125
|
+
const src = `// ctx.libs.lodash\nconst s = "ctx.libs.lodash";\nreturn s;`;
|
|
126
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
127
|
+
expect(out).not.toContain(`__runjs_ensure_libs`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('is idempotent for already-prepared code', async () => {
|
|
131
|
+
const src = `return ctx.libs['lodash'];`;
|
|
132
|
+
const out1 = await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
133
|
+
const out2 = await prepareRunJsCode(out1, { preprocessTemplates: false });
|
|
134
|
+
expect(out2).toBe(out1);
|
|
135
|
+
expect(out2.match(/__runjs_ensure_libs/g)?.length ?? 0).toBe(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not break JSX attribute string values when preprocessing templates', async () => {
|
|
139
|
+
const src = `ctx.render(<Input title="{{ctx.user.name}}" />);`;
|
|
140
|
+
const out = await prepareRunJsCode(src, { preprocessTemplates: true });
|
|
141
|
+
expect(out).toMatch(/ctx\.React\.createElement/);
|
|
142
|
+
expect(out).toMatch(/title:\s*\(/);
|
|
143
|
+
expect(out).toContain(`.split("{{ctx.user.name}}").join(`);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('caches prepared code by source and preprocessTemplates option', async () => {
|
|
147
|
+
const spy = vi.spyOn(jsxTransform, 'compileRunJs');
|
|
148
|
+
const src = `/* cache-test */\nconst a = 1;\nreturn a;`;
|
|
149
|
+
|
|
150
|
+
await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
151
|
+
await prepareRunJsCode(src, { preprocessTemplates: false });
|
|
152
|
+
await prepareRunJsCode(src, { preprocessTemplates: true });
|
|
153
|
+
await prepareRunJsCode(src, { preprocessTemplates: true });
|
|
154
|
+
|
|
155
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
156
|
+
spy.mockRestore();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { extractUsedVariablePathsFromRunJS, isRunJSValue, normalizeRunJSValue } from '../runjsValue';
|
|
12
|
+
|
|
13
|
+
describe('runjsValue utils', () => {
|
|
14
|
+
it('isRunJSValue: strict shape detection', () => {
|
|
15
|
+
expect(isRunJSValue({ code: 'return 1' })).toBe(true);
|
|
16
|
+
expect(isRunJSValue({ code: 'return 1', version: 'v1' })).toBe(true);
|
|
17
|
+
|
|
18
|
+
expect(isRunJSValue(null)).toBe(false);
|
|
19
|
+
expect(isRunJSValue('return 1')).toBe(false);
|
|
20
|
+
expect(isRunJSValue({})).toBe(false);
|
|
21
|
+
expect(isRunJSValue({ version: 'v1' })).toBe(false);
|
|
22
|
+
expect(isRunJSValue({ code: 1 })).toBe(false);
|
|
23
|
+
expect(isRunJSValue({ code: 'return 1', foo: 1 })).toBe(false);
|
|
24
|
+
expect(isRunJSValue([])).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('normalizeRunJSValue: defaults version to v1', () => {
|
|
28
|
+
expect(normalizeRunJSValue({ code: 'return 1' })).toEqual({ code: 'return 1', version: 'v1' });
|
|
29
|
+
expect(normalizeRunJSValue({ code: 'return 1', version: 'v2' })).toEqual({ code: 'return 1', version: 'v2' });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('extractUsedVariablePathsFromRunJS: extracts ctx usage (dot + bracket root)', () => {
|
|
33
|
+
const code = `
|
|
34
|
+
// comment: ctx.ignore.me
|
|
35
|
+
const x = "ctx.ignore.too";
|
|
36
|
+
return ctx.formValues.a + ctx.record.id + ctx.someVar + ctx['user'].name;
|
|
37
|
+
`;
|
|
38
|
+
const out = extractUsedVariablePathsFromRunJS(code);
|
|
39
|
+
expect(out.formValues).toContain('a');
|
|
40
|
+
expect(out.record).toContain('id');
|
|
41
|
+
expect(out.someVar).toContain('');
|
|
42
|
+
expect(out.user).toContain('name');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -7,8 +7,19 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { describe, expect, it } from 'vitest';
|
|
11
|
-
import {
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
__resetRunJSSafeGlobalsRegistryForTests,
|
|
13
|
+
createSafeDocument,
|
|
14
|
+
createSafeNavigator,
|
|
15
|
+
createSafeWindow,
|
|
16
|
+
registerRunJSSafeDocumentGlobals,
|
|
17
|
+
registerRunJSSafeWindowGlobals,
|
|
18
|
+
} from '../safeGlobals';
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
__resetRunJSSafeGlobalsRegistryForTests();
|
|
22
|
+
});
|
|
12
23
|
|
|
13
24
|
describe('safeGlobals', () => {
|
|
14
25
|
it('createSafeWindow exposes only allowed globals and extras', () => {
|
|
@@ -17,10 +28,36 @@ describe('safeGlobals', () => {
|
|
|
17
28
|
expect(win.console).toBeDefined();
|
|
18
29
|
expect(win.foo).toBe(123);
|
|
19
30
|
expect(new win.FormData()).toBeInstanceOf(window.FormData);
|
|
31
|
+
if (typeof window.Blob !== 'undefined') {
|
|
32
|
+
expect(typeof win.Blob).toBe('function');
|
|
33
|
+
expect(new win.Blob(['x'])).toBeInstanceOf(window.Blob);
|
|
34
|
+
}
|
|
35
|
+
if (typeof window.URL !== 'undefined') {
|
|
36
|
+
expect(win.URL).toBe(window.URL);
|
|
37
|
+
expect(typeof win.URL.createObjectURL).toBe('function');
|
|
38
|
+
}
|
|
20
39
|
// access to location proxy is allowed, but sensitive props throw
|
|
21
40
|
expect(() => win.location.href).toThrow(/not allowed/);
|
|
22
41
|
});
|
|
23
42
|
|
|
43
|
+
it('createSafeWindow allows writing new props and reading them back', () => {
|
|
44
|
+
const win: any = createSafeWindow();
|
|
45
|
+
win.someLib = { ok: true };
|
|
46
|
+
expect(win.someLib).toEqual({ ok: true });
|
|
47
|
+
expect('someLib' in win).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('createSafeWindow can access dynamically registered globals from real window', () => {
|
|
51
|
+
const key = '__nb_safe_window_global__';
|
|
52
|
+
(window as any)[key] = { v: 1 };
|
|
53
|
+
registerRunJSSafeWindowGlobals([key]);
|
|
54
|
+
|
|
55
|
+
const win: any = createSafeWindow();
|
|
56
|
+
expect(win[key]).toEqual({ v: 1 });
|
|
57
|
+
|
|
58
|
+
delete (window as any)[key];
|
|
59
|
+
});
|
|
60
|
+
|
|
24
61
|
it('createSafeDocument exposes whitelisted methods and extras', () => {
|
|
25
62
|
const doc: any = createSafeDocument({ bar: true });
|
|
26
63
|
expect(typeof doc.createElement).toBe('function');
|
|
@@ -28,6 +65,24 @@ describe('safeGlobals', () => {
|
|
|
28
65
|
expect(() => doc.cookie).toThrow(/not allowed/);
|
|
29
66
|
});
|
|
30
67
|
|
|
68
|
+
it('createSafeDocument allows writing new props and reading them back', () => {
|
|
69
|
+
const doc: any = createSafeDocument();
|
|
70
|
+
doc.someLib = { ok: true };
|
|
71
|
+
expect(doc.someLib).toEqual({ ok: true });
|
|
72
|
+
expect('someLib' in doc).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('createSafeDocument can access dynamically registered globals from real document', () => {
|
|
76
|
+
const key = '__nb_safe_document_global__';
|
|
77
|
+
(document as any)[key] = 123;
|
|
78
|
+
registerRunJSSafeDocumentGlobals([key]);
|
|
79
|
+
|
|
80
|
+
const doc: any = createSafeDocument();
|
|
81
|
+
expect(doc[key]).toBe(123);
|
|
82
|
+
|
|
83
|
+
delete (document as any)[key];
|
|
84
|
+
});
|
|
85
|
+
|
|
31
86
|
it('createSafeNavigator exposes limited props and guards others', () => {
|
|
32
87
|
const nav: any = createSafeNavigator();
|
|
33
88
|
// clipboard object should always exist
|