@nocobase/flow-engine 2.1.0-beta.14 → 2.1.0-beta.16
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/components/FlowModelRenderer.js +10 -6
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +13 -5
- package/lib/components/subModel/AddSubModelButton.js +1 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/flowEngine.d.ts +132 -1
- package/lib/flowEngine.js +360 -14
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/models/flowModel.d.ts +2 -1
- package/lib/models/flowModel.js +28 -9
- package/lib/types.d.ts +46 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/package.json +4 -4
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
- package/src/components/FlowModelRenderer.tsx +9 -5
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +15 -4
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +67 -5
- package/src/components/subModel/AddSubModelButton.tsx +1 -1
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +93 -33
- package/src/components/subModel/utils.ts +1 -1
- package/src/flowEngine.ts +412 -10
- package/src/flowSettings.ts +40 -6
- package/src/lazy-helper.tsx +57 -0
- package/src/models/flowModel.tsx +31 -10
- package/src/types.ts +59 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
|
@@ -13,6 +13,7 @@ import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContex
|
|
|
13
13
|
import { FlowEngine } from '../flowEngine';
|
|
14
14
|
import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, ResolveUseResult, StepParams } from '../types';
|
|
15
15
|
import { IModelComponentProps, ReadonlyModelProps } from '../types';
|
|
16
|
+
import type { MenuProps } from 'antd';
|
|
16
17
|
import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
|
|
17
18
|
import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
|
|
18
19
|
import { GlobalFlowRegistry } from '../flow-registry/GlobalFlowRegistry';
|
|
@@ -21,7 +22,6 @@ import { FlowSettingsOpenOptions } from '../flowSettings';
|
|
|
21
22
|
import type { ScheduleOptions } from '../scheduler/ModelOperationScheduler';
|
|
22
23
|
import type { DispatchEventOptions, EventDefinition } from '../types';
|
|
23
24
|
import { ForkFlowModel } from './forkFlowModel';
|
|
24
|
-
import type { MenuProps } from 'antd';
|
|
25
25
|
type BaseMenuItem = NonNullable<MenuProps['items']>[number];
|
|
26
26
|
type MenuLeafItem = Exclude<BaseMenuItem, {
|
|
27
27
|
children: MenuProps['items'];
|
|
@@ -239,6 +239,7 @@ export declare class FlowModel<Structure extends DefaultStructure = DefaultStruc
|
|
|
239
239
|
* 使用 lodash debounce 避免频繁调用
|
|
240
240
|
*/
|
|
241
241
|
private _rerunLastAutoRun;
|
|
242
|
+
private resetAutoRunState;
|
|
242
243
|
/**
|
|
243
244
|
* 通用事件分发钩子:开始
|
|
244
245
|
* 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
|
package/lib/models/flowModel.js
CHANGED
|
@@ -56,25 +56,33 @@ var import_reactive = require("@formily/reactive");
|
|
|
56
56
|
var import_lodash = __toESM(require("lodash"));
|
|
57
57
|
var import_react = __toESM(require("react"));
|
|
58
58
|
var import_secure = require("uid/secure");
|
|
59
|
-
var import_StepRequiredSettingsDialog = require("../components/settings/wrappers/contextual/StepRequiredSettingsDialog");
|
|
60
|
-
var import_StepSettingsDialog = require("../components/settings/wrappers/contextual/StepSettingsDialog");
|
|
61
59
|
var import_emitter = require("../emitter");
|
|
62
60
|
var import_InstanceFlowRegistry = require("../flow-registry/InstanceFlowRegistry");
|
|
63
61
|
var import_flowContext = require("../flowContext");
|
|
64
62
|
var import_utils = require("../utils");
|
|
65
|
-
var
|
|
63
|
+
var import_antd = require("antd");
|
|
64
|
+
var import__ = require("..");
|
|
66
65
|
var import_ModelActionRegistry = require("../action-registry/ModelActionRegistry");
|
|
67
66
|
var import_utils2 = require("../components/subModel/utils");
|
|
68
67
|
var import_ModelEventRegistry = require("../event-registry/ModelEventRegistry");
|
|
69
68
|
var import_GlobalFlowRegistry = require("../flow-registry/GlobalFlowRegistry");
|
|
70
69
|
var import_forkFlowModel = require("./forkFlowModel");
|
|
71
|
-
var import__ = require("..");
|
|
72
70
|
var _flowContext;
|
|
73
71
|
const classActionRegistries = /* @__PURE__ */ new WeakMap();
|
|
74
72
|
const classEventRegistries = /* @__PURE__ */ new WeakMap();
|
|
75
73
|
const modelMetas = /* @__PURE__ */ new WeakMap();
|
|
76
74
|
const modelGlobalRegistries = /* @__PURE__ */ new WeakMap();
|
|
77
75
|
const classMenuExtensions = /* @__PURE__ */ new WeakMap();
|
|
76
|
+
async function loadOpenStepSettingsDialog() {
|
|
77
|
+
const mod = await import("../components/settings/wrappers/contextual/StepSettingsDialog");
|
|
78
|
+
return mod.openStepSettingsDialog;
|
|
79
|
+
}
|
|
80
|
+
__name(loadOpenStepSettingsDialog, "loadOpenStepSettingsDialog");
|
|
81
|
+
async function loadOpenRequiredParamsStepFormDialog() {
|
|
82
|
+
const mod = await import("../components/settings/wrappers/contextual/StepRequiredSettingsDialog");
|
|
83
|
+
return mod.openRequiredParamsStepFormDialog;
|
|
84
|
+
}
|
|
85
|
+
__name(loadOpenRequiredParamsStepFormDialog, "loadOpenRequiredParamsStepFormDialog");
|
|
78
86
|
var ModelRenderMode = /* @__PURE__ */ ((ModelRenderMode2) => {
|
|
79
87
|
ModelRenderMode2["ReactElement"] = "reactElement";
|
|
80
88
|
ModelRenderMode2["RenderFunction"] = "renderFunction";
|
|
@@ -183,10 +191,13 @@ const _FlowModel = class _FlowModel {
|
|
|
183
191
|
if (changed.type === "set" && import_lodash.default.isEqual(changed.value, changed.oldValue)) {
|
|
184
192
|
return;
|
|
185
193
|
}
|
|
194
|
+
const hasLastAutoRun = !!this._lastAutoRunParams;
|
|
186
195
|
if (this.flowEngine) {
|
|
187
196
|
this.invalidateFlowCache("beforeRender");
|
|
188
197
|
}
|
|
189
|
-
|
|
198
|
+
if (hasLastAutoRun) {
|
|
199
|
+
this._rerunLastAutoRun();
|
|
200
|
+
}
|
|
190
201
|
this.forks.forEach((fork) => {
|
|
191
202
|
fork.rerender();
|
|
192
203
|
});
|
|
@@ -674,6 +685,11 @@ const _FlowModel = class _FlowModel {
|
|
|
674
685
|
}, "isMatch");
|
|
675
686
|
return Array.from(allFlows.values()).filter(isMatch);
|
|
676
687
|
}
|
|
688
|
+
resetAutoRunState() {
|
|
689
|
+
var _a, _b;
|
|
690
|
+
(_b = (_a = this._rerunLastAutoRun) == null ? void 0 : _a.cancel) == null ? void 0 : _b.call(_a);
|
|
691
|
+
this._lastAutoRunParams = null;
|
|
692
|
+
}
|
|
677
693
|
/**
|
|
678
694
|
* 通用事件分发钩子:开始
|
|
679
695
|
* 子类可覆盖;beforeRender 事件可通过抛出 FlowExitException 提前终止。
|
|
@@ -763,6 +779,7 @@ const _FlowModel = class _FlowModel {
|
|
|
763
779
|
}));
|
|
764
780
|
return () => {
|
|
765
781
|
var _a3, _b3;
|
|
782
|
+
renderTarget.resetAutoRunState();
|
|
766
783
|
if (typeof renderTarget.onUnmount === "function") {
|
|
767
784
|
renderTarget.onUnmount();
|
|
768
785
|
}
|
|
@@ -1058,7 +1075,7 @@ const _FlowModel = class _FlowModel {
|
|
|
1058
1075
|
* @param {string} stepKey 步骤的唯一标识符
|
|
1059
1076
|
* @returns {void}
|
|
1060
1077
|
*/
|
|
1061
|
-
openStepSettingsDialog(flowKey, stepKey) {
|
|
1078
|
+
async openStepSettingsDialog(flowKey, stepKey) {
|
|
1062
1079
|
var _a;
|
|
1063
1080
|
const flow = this.getFlow(flowKey);
|
|
1064
1081
|
const step = (_a = flow == null ? void 0 : flow.steps) == null ? void 0 : _a[stepKey];
|
|
@@ -1069,7 +1086,8 @@ const _FlowModel = class _FlowModel {
|
|
|
1069
1086
|
const ctx = new import_flowContext.FlowRuntimeContext(this, flowKey, "settings");
|
|
1070
1087
|
(0, import_utils.setupRuntimeContextSteps)(ctx, flow.steps, this, flowKey);
|
|
1071
1088
|
ctx.defineProperty("currentStep", { value: step });
|
|
1072
|
-
|
|
1089
|
+
const openStepSettingsDialog = await loadOpenStepSettingsDialog();
|
|
1090
|
+
return openStepSettingsDialog({
|
|
1073
1091
|
model: this,
|
|
1074
1092
|
flowKey,
|
|
1075
1093
|
stepKey,
|
|
@@ -1084,7 +1102,8 @@ const _FlowModel = class _FlowModel {
|
|
|
1084
1102
|
* @returns {Promise<any>} 返回表单提交的值
|
|
1085
1103
|
*/
|
|
1086
1104
|
async configureRequiredSteps(dialogWidth, dialogTitle) {
|
|
1087
|
-
|
|
1105
|
+
const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
|
|
1106
|
+
return openRequiredParamsStepFormDialog({
|
|
1088
1107
|
model: this,
|
|
1089
1108
|
dialogWidth,
|
|
1090
1109
|
dialogTitle
|
|
@@ -1303,7 +1322,7 @@ const _ErrorFlowModel = class _ErrorFlowModel extends FlowModel {
|
|
|
1303
1322
|
this.errorMessage = msg;
|
|
1304
1323
|
}
|
|
1305
1324
|
render() {
|
|
1306
|
-
return /* @__PURE__ */ import_react.default.createElement(
|
|
1325
|
+
return /* @__PURE__ */ import_react.default.createElement(import_antd.Typography.Text, { type: "danger" }, this.errorMessage);
|
|
1307
1326
|
}
|
|
1308
1327
|
};
|
|
1309
1328
|
__name(_ErrorFlowModel, "ErrorFlowModel");
|
package/lib/types.d.ts
CHANGED
|
@@ -303,6 +303,52 @@ export interface CreateModelOptions {
|
|
|
303
303
|
delegateToParent?: boolean;
|
|
304
304
|
[key: string]: any;
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* FlowModel loader result.
|
|
308
|
+
* Supports returning the model constructor directly, a default export, or a module object containing the named export.
|
|
309
|
+
*/
|
|
310
|
+
export type FlowModelLoaderResult = ModelConstructor | {
|
|
311
|
+
default?: ModelConstructor;
|
|
312
|
+
[key: string]: unknown;
|
|
313
|
+
} | Record<string, unknown>;
|
|
314
|
+
/**
|
|
315
|
+
* FlowModel loader function.
|
|
316
|
+
*/
|
|
317
|
+
export type FlowModelLoader = () => Promise<FlowModelLoaderResult>;
|
|
318
|
+
/**
|
|
319
|
+
* FlowModel loader entry (normalized internal form).
|
|
320
|
+
*/
|
|
321
|
+
export interface FlowModelLoaderEntry {
|
|
322
|
+
loader: FlowModelLoader;
|
|
323
|
+
extends?: string[];
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* FlowModel loader input (user-facing form for registerModelLoaders).
|
|
327
|
+
* The `extends` field accepts flexible formats that will be normalized to `string[]` at registration time.
|
|
328
|
+
*/
|
|
329
|
+
export interface FlowModelLoaderInput {
|
|
330
|
+
loader: FlowModelLoader;
|
|
331
|
+
extends?: string | ModelConstructor | (string | ModelConstructor)[];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* FlowModel loader entry map (normalized internal form).
|
|
335
|
+
*/
|
|
336
|
+
export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
|
|
337
|
+
/**
|
|
338
|
+
* FlowModel loader input map (user-facing form for registerModelLoaders).
|
|
339
|
+
*/
|
|
340
|
+
export type FlowModelLoaderInputMap = Record<string, FlowModelLoaderInput>;
|
|
341
|
+
/**
|
|
342
|
+
* Batch ensure result.
|
|
343
|
+
*/
|
|
344
|
+
export interface EnsureBatchResult {
|
|
345
|
+
requested: string[];
|
|
346
|
+
loaded: string[];
|
|
347
|
+
failed: Array<{
|
|
348
|
+
name: string;
|
|
349
|
+
error?: unknown;
|
|
350
|
+
}>;
|
|
351
|
+
}
|
|
306
352
|
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
|
307
353
|
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
|
308
354
|
save(model: T, options?: {
|
|
@@ -527,8 +527,8 @@ function extractUsedCtxLibKeys(code) {
|
|
|
527
527
|
}
|
|
528
528
|
__name(extractUsedCtxLibKeys, "extractUsedCtxLibKeys");
|
|
529
529
|
function injectEnsureLibsPreamble(code) {
|
|
530
|
-
if (!CTX_LIBS_MARKER_RE.test(code)) return code;
|
|
531
530
|
if (ENSURE_LIBS_MARKER_RE.test(code)) return code;
|
|
531
|
+
if (!CTX_LIBS_MARKER_RE.test(code)) return code;
|
|
532
532
|
const keys = extractUsedCtxLibKeys(code);
|
|
533
533
|
if (!keys.length) return code;
|
|
534
534
|
return `/* __runjs_ensure_libs */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/flow-engine",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@formily/antd-v5": "1.x",
|
|
10
10
|
"@formily/reactive": "2.x",
|
|
11
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
12
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-beta.16",
|
|
12
|
+
"@nocobase/shared": "2.1.0-beta.16",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"axios": "^1.7.0",
|
|
15
15
|
"dayjs": "^1.11.9",
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "NocoBase Team",
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "b9a191705a440a336c85d82fd877fdf152bec70f"
|
|
41
41
|
}
|
|
@@ -189,4 +189,170 @@ describe('FlowEngine', () => {
|
|
|
189
189
|
expect(mounted?.uid).toBe('c3');
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
describe('getSubclassesOfAsync', () => {
|
|
194
|
+
it('should return async-loaded subclasses matching extends declaration', async () => {
|
|
195
|
+
class AsyncSubModelD extends BaseModel {}
|
|
196
|
+
class AsyncSubModelE extends BaseModel {}
|
|
197
|
+
|
|
198
|
+
engine.registerModelLoaders({
|
|
199
|
+
AsyncSubModelD: {
|
|
200
|
+
extends: 'BaseModel',
|
|
201
|
+
loader: async () => ({ AsyncSubModelD }),
|
|
202
|
+
},
|
|
203
|
+
AsyncSubModelE: {
|
|
204
|
+
extends: 'BaseModel',
|
|
205
|
+
loader: async () => ({ AsyncSubModelE }),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
210
|
+
|
|
211
|
+
// Sync-registered subclasses
|
|
212
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
213
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
214
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
215
|
+
// Async-loaded subclasses
|
|
216
|
+
expect(result.has('AsyncSubModelD')).toBe(true);
|
|
217
|
+
expect(result.has('AsyncSubModelE')).toBe(true);
|
|
218
|
+
// Base class excluded
|
|
219
|
+
expect(result.has('BaseModel')).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should merge sync-registered and async-loaded subclasses', async () => {
|
|
223
|
+
class AsyncSubModel extends BaseModel {}
|
|
224
|
+
|
|
225
|
+
engine.registerModelLoaders({
|
|
226
|
+
AsyncSubModel: {
|
|
227
|
+
extends: 'BaseModel',
|
|
228
|
+
loader: async () => ({ AsyncSubModel }),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await engine.getSubclassesOfAsync('BaseModel');
|
|
233
|
+
|
|
234
|
+
// Sync: SubModelA, SubModelB, SubModelC
|
|
235
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
236
|
+
expect(result.has('SubModelB')).toBe(true);
|
|
237
|
+
expect(result.has('SubModelC')).toBe(true);
|
|
238
|
+
// Async
|
|
239
|
+
expect(result.has('AsyncSubModel')).toBe(true);
|
|
240
|
+
expect(result.size).toBe(4);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should support extends as string array (multiple parents)', async () => {
|
|
244
|
+
class AnotherBase extends FlowModel {}
|
|
245
|
+
class MultiParentModel extends BaseModel {}
|
|
246
|
+
|
|
247
|
+
engine.registerModels({ AnotherBase });
|
|
248
|
+
engine.registerModelLoaders({
|
|
249
|
+
MultiParentModel: {
|
|
250
|
+
extends: ['BaseModel', 'AnotherBase'],
|
|
251
|
+
loader: async () => ({ MultiParentModel }),
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const resultBase = await engine.getSubclassesOfAsync(BaseModel);
|
|
256
|
+
expect(resultBase.has('MultiParentModel')).toBe(true);
|
|
257
|
+
|
|
258
|
+
// Also found by AnotherBase (even though actual inheritance is from BaseModel, not AnotherBase)
|
|
259
|
+
// The extends declaration triggers loading, but isInheritedFrom validation will exclude it from AnotherBase results
|
|
260
|
+
const resultAnother = await engine.getSubclassesOfAsync(AnotherBase);
|
|
261
|
+
expect(resultAnother.has('MultiParentModel')).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should support extends as ModelConstructor', async () => {
|
|
265
|
+
class AsyncCtorSubModel extends BaseModel {}
|
|
266
|
+
|
|
267
|
+
engine.registerModelLoaders({
|
|
268
|
+
AsyncCtorSubModel: {
|
|
269
|
+
extends: BaseModel,
|
|
270
|
+
loader: async () => ({ AsyncCtorSubModel }),
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
275
|
+
expect(result.has('AsyncCtorSubModel')).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should validate actual inheritance and warn on mismatch', async () => {
|
|
279
|
+
class UnrelatedModel extends FlowModel {}
|
|
280
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
281
|
+
|
|
282
|
+
engine.registerModelLoaders({
|
|
283
|
+
UnrelatedModel: {
|
|
284
|
+
extends: 'BaseModel',
|
|
285
|
+
loader: async () => ({ UnrelatedModel }),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
290
|
+
expect(result.has('UnrelatedModel')).toBe(false);
|
|
291
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
292
|
+
expect.stringContaining("declares extends 'BaseModel' but does not actually inherit from it"),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
warnSpy.mockRestore();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should resolve base class from loaders if not in _modelClasses', async () => {
|
|
299
|
+
const freshEngine = new FlowEngine();
|
|
300
|
+
|
|
301
|
+
class LazyBase extends FlowModel {}
|
|
302
|
+
class LazySub extends LazyBase {}
|
|
303
|
+
|
|
304
|
+
freshEngine.registerModelLoaders({
|
|
305
|
+
LazyBase: {
|
|
306
|
+
loader: async () => ({ LazyBase }),
|
|
307
|
+
},
|
|
308
|
+
LazySub: {
|
|
309
|
+
extends: 'LazyBase',
|
|
310
|
+
loader: async () => ({ LazySub }),
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const result = await freshEngine.getSubclassesOfAsync('LazyBase');
|
|
315
|
+
expect(result.has('LazySub')).toBe(true);
|
|
316
|
+
expect(result.size).toBe(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should return empty Map when base class cannot be found', async () => {
|
|
320
|
+
const result = await engine.getSubclassesOfAsync('NonExistentModel');
|
|
321
|
+
expect(result.size).toBe(0);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should support filter parameter on both sync and async sources', async () => {
|
|
325
|
+
class FilteredAsyncModel extends BaseModel {}
|
|
326
|
+
|
|
327
|
+
engine.registerModelLoaders({
|
|
328
|
+
FilteredAsyncModel: {
|
|
329
|
+
extends: 'BaseModel',
|
|
330
|
+
loader: async () => ({ FilteredAsyncModel }),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = await engine.getSubclassesOfAsync(BaseModel, (_ModelClass, name) => name.startsWith('SubModelA'));
|
|
335
|
+
|
|
336
|
+
// Only SubModelA passes the filter (SubModelB, SubModelC, FilteredAsyncModel excluded)
|
|
337
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
338
|
+
expect(result.has('SubModelB')).toBe(false);
|
|
339
|
+
expect(result.has('SubModelC')).toBe(false);
|
|
340
|
+
expect(result.has('FilteredAsyncModel')).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should not include loaders without extends declaration', async () => {
|
|
344
|
+
class NoExtendsModel extends BaseModel {}
|
|
345
|
+
|
|
346
|
+
engine.registerModelLoaders({
|
|
347
|
+
NoExtendsModel: {
|
|
348
|
+
loader: async () => ({ NoExtendsModel }),
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const result = await engine.getSubclassesOfAsync(BaseModel);
|
|
353
|
+
// Only sync-registered subclasses; NoExtendsModel has no extends, so not discovered
|
|
354
|
+
expect(result.has('NoExtendsModel')).toBe(false);
|
|
355
|
+
expect(result.has('SubModelA')).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
192
358
|
});
|
|
@@ -0,0 +1,245 @@
|
|
|
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, vi } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '../flowEngine';
|
|
12
|
+
import { ErrorFlowModel, FlowModel } from '../models';
|
|
13
|
+
import type { IFlowModelRepository } from '../types';
|
|
14
|
+
|
|
15
|
+
class MockFlowModelRepository implements IFlowModelRepository {
|
|
16
|
+
findOneResult: any = null;
|
|
17
|
+
save = vi.fn(async (model: FlowModel) => ({ success: true, uid: model.uid }));
|
|
18
|
+
|
|
19
|
+
async findOne() {
|
|
20
|
+
return this.findOneResult ? JSON.parse(JSON.stringify(this.findOneResult)) : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async destroy() {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async move() {}
|
|
28
|
+
|
|
29
|
+
async duplicate() {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('FlowEngine model loaders', () => {
|
|
35
|
+
let engine: FlowEngine;
|
|
36
|
+
let repo: MockFlowModelRepository;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
engine = new FlowEngine();
|
|
40
|
+
repo = new MockFlowModelRepository();
|
|
41
|
+
engine.setModelRepository(repo);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('resolves explicit and meta-default model trees before synchronous creation', async () => {
|
|
45
|
+
class ParentModel extends FlowModel {}
|
|
46
|
+
class ChildModel extends FlowModel {}
|
|
47
|
+
class DefaultChildModel extends FlowModel {}
|
|
48
|
+
|
|
49
|
+
ParentModel.define({
|
|
50
|
+
createModelOptions: {
|
|
51
|
+
subModels: {
|
|
52
|
+
defaultChild: {
|
|
53
|
+
use: 'DefaultChildModel',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
60
|
+
const childLoader = vi.fn(async () => ({ ChildModel }));
|
|
61
|
+
const defaultChildLoader = vi.fn(async () => ({ DefaultChildModel }));
|
|
62
|
+
|
|
63
|
+
engine.registerModelLoaders({
|
|
64
|
+
ParentModel: { loader: parentLoader },
|
|
65
|
+
ChildModel: { loader: childLoader },
|
|
66
|
+
DefaultChildModel: { loader: defaultChildLoader },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const model = await engine.loadOrCreateModel({
|
|
70
|
+
uid: 'parent-model',
|
|
71
|
+
use: 'ParentModel',
|
|
72
|
+
subModels: {
|
|
73
|
+
child: {
|
|
74
|
+
use: 'ChildModel',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
80
|
+
expect(model?.subModels.child).toBeInstanceOf(ChildModel);
|
|
81
|
+
expect(model?.subModels.defaultChild).toBeInstanceOf(DefaultChildModel);
|
|
82
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(defaultChildLoader).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('resolves repository-loaded model trees before loadModel creates instances', async () => {
|
|
88
|
+
class RepoRootModel extends FlowModel {}
|
|
89
|
+
class RepoChildModel extends FlowModel {}
|
|
90
|
+
|
|
91
|
+
const rootLoader = vi.fn(async () => ({ RepoRootModel }));
|
|
92
|
+
const childLoader = vi.fn(async () => ({ RepoChildModel }));
|
|
93
|
+
|
|
94
|
+
engine.registerModelLoaders({
|
|
95
|
+
RepoRootModel: { loader: rootLoader },
|
|
96
|
+
RepoChildModel: { loader: childLoader },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
repo.findOneResult = {
|
|
100
|
+
uid: 'repo-root',
|
|
101
|
+
use: 'RepoRootModel',
|
|
102
|
+
subModels: {
|
|
103
|
+
child: {
|
|
104
|
+
use: 'RepoChildModel',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const model = await engine.loadModel({ uid: 'repo-root' });
|
|
110
|
+
|
|
111
|
+
expect(model).toBeInstanceOf(RepoRootModel);
|
|
112
|
+
expect(model?.subModels.child).toBeInstanceOf(RepoChildModel);
|
|
113
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('supports async model creation and async getters', async () => {
|
|
118
|
+
class AsyncRootModel extends FlowModel {}
|
|
119
|
+
class AsyncChildModel extends FlowModel {}
|
|
120
|
+
|
|
121
|
+
const rootLoader = vi.fn(async () => ({ AsyncRootModel }));
|
|
122
|
+
const childLoader = vi.fn(async () => ({ AsyncChildModel }));
|
|
123
|
+
|
|
124
|
+
engine.registerModelLoaders({
|
|
125
|
+
AsyncRootModel: { loader: rootLoader },
|
|
126
|
+
AsyncChildModel: { loader: childLoader },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const rootClass = await engine.getModelClassAsync('AsyncRootModel');
|
|
130
|
+
const classes = await engine.getModelClassesAsync();
|
|
131
|
+
const model = await engine.createModelAsync({
|
|
132
|
+
uid: 'async-root',
|
|
133
|
+
use: 'AsyncRootModel',
|
|
134
|
+
subModels: {
|
|
135
|
+
child: {
|
|
136
|
+
use: 'AsyncChildModel',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(rootClass).toBe(AsyncRootModel);
|
|
142
|
+
expect(classes.get('AsyncRootModel')).toBe(AsyncRootModel);
|
|
143
|
+
expect(classes.get('AsyncChildModel')).toBe(AsyncChildModel);
|
|
144
|
+
expect(model).toBeInstanceOf(AsyncRootModel);
|
|
145
|
+
expect(model.subModels.child).toBeInstanceOf(AsyncChildModel);
|
|
146
|
+
expect(rootLoader).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(childLoader).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('createModelAsync degrades unresolved loader failures to ErrorFlowModel', async () => {
|
|
151
|
+
const invalidLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
152
|
+
|
|
153
|
+
engine.registerModelLoaders({
|
|
154
|
+
BrokenRootModel: { loader: invalidLoader as any },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const model = await engine.createModelAsync({
|
|
158
|
+
uid: 'broken-root',
|
|
159
|
+
use: 'BrokenRootModel',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(model).toBeInstanceOf(ErrorFlowModel);
|
|
163
|
+
expect(invalidLoader).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('keeps loader resolution idempotent across resolveModelTree and flow settings preload', async () => {
|
|
167
|
+
class RuntimeResolvedModel extends FlowModel {}
|
|
168
|
+
class DesignResolvedModel extends FlowModel {}
|
|
169
|
+
|
|
170
|
+
const runtimeLoader = vi.fn(async () => ({ RuntimeResolvedModel }));
|
|
171
|
+
const designLoader = vi.fn(async () => ({ DesignResolvedModel }));
|
|
172
|
+
|
|
173
|
+
engine.registerModelLoaders({
|
|
174
|
+
RuntimeResolvedModel: { loader: runtimeLoader },
|
|
175
|
+
DesignResolvedModel: { loader: designLoader },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await engine.resolveModelTree({
|
|
179
|
+
use: 'RuntimeResolvedModel',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const firstPreload = await engine.preloadModelLoaders();
|
|
183
|
+
const secondPreload = await engine.preloadModelLoaders();
|
|
184
|
+
|
|
185
|
+
expect(runtimeLoader).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(designLoader).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(firstPreload.loaded).toContain('DesignResolvedModel');
|
|
188
|
+
expect(firstPreload.loaded).not.toContain('RuntimeResolvedModel');
|
|
189
|
+
expect(secondPreload.loaded).toHaveLength(0);
|
|
190
|
+
expect(secondPreload.failed).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('picks up newly registered loaders after preload has already completed', async () => {
|
|
194
|
+
class FirstModel extends FlowModel {}
|
|
195
|
+
class SecondModel extends FlowModel {}
|
|
196
|
+
|
|
197
|
+
const firstLoader = vi.fn(async () => ({ FirstModel }));
|
|
198
|
+
const secondLoader = vi.fn(async () => ({ SecondModel }));
|
|
199
|
+
|
|
200
|
+
engine.registerModelLoaders({
|
|
201
|
+
FirstModel: { loader: firstLoader },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await engine.preloadModelLoaders();
|
|
205
|
+
expect(firstLoader).toHaveBeenCalledTimes(1);
|
|
206
|
+
expect(engine.getModelClass('FirstModel')).toBe(FirstModel);
|
|
207
|
+
|
|
208
|
+
engine.registerModelLoaders({
|
|
209
|
+
SecondModel: { loader: secondLoader },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = await engine.preloadModelLoaders();
|
|
213
|
+
|
|
214
|
+
expect(secondLoader).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(result.loaded).toContain('SecondModel');
|
|
216
|
+
expect(engine.getModelClass('SecondModel')).toBe(SecondModel);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('degrades unresolved loader failures to ErrorFlowModel instead of crashing runtime creation', async () => {
|
|
220
|
+
class ParentModel extends FlowModel {}
|
|
221
|
+
|
|
222
|
+
const parentLoader = vi.fn(async () => ({ ParentModel }));
|
|
223
|
+
const invalidChildLoader = vi.fn(async () => ({ notAModel: {} }));
|
|
224
|
+
|
|
225
|
+
engine.registerModelLoaders({
|
|
226
|
+
ParentModel: { loader: parentLoader },
|
|
227
|
+
BrokenChildModel: { loader: invalidChildLoader as any },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const model = await engine.loadOrCreateModel({
|
|
231
|
+
uid: 'parent-with-broken-child',
|
|
232
|
+
use: 'ParentModel',
|
|
233
|
+
subModels: {
|
|
234
|
+
child: {
|
|
235
|
+
use: 'BrokenChildModel',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(model).toBeInstanceOf(ParentModel);
|
|
241
|
+
expect(model?.subModels.child).toBeInstanceOf(ErrorFlowModel);
|
|
242
|
+
expect(parentLoader).toHaveBeenCalledTimes(1);
|
|
243
|
+
expect(invalidChildLoader).toHaveBeenCalledTimes(1);
|
|
244
|
+
});
|
|
245
|
+
});
|