@nocobase/flow-engine 2.0.22 → 2.1.0-alpha.10
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/MobilePopup.js +6 -5
- package/lib/components/subModel/AddSubModelButton.js +1 -1
- package/lib/components/subModel/utils.js +2 -2
- package/lib/flowEngine.d.ts +120 -1
- package/lib/flowEngine.js +301 -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.js +17 -7
- package/lib/types.d.ts +35 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/package.json +4 -4
- 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/MobilePopup.tsx +4 -2
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
- 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 +338 -10
- package/src/flowSettings.ts +40 -6
- package/src/lazy-helper.tsx +57 -0
- package/src/models/flowModel.tsx +18 -6
- package/src/types.ts +47 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
package/lib/flowSettings.js
CHANGED
|
@@ -65,6 +65,7 @@ var import_utils = require("./utils");
|
|
|
65
65
|
var import_exceptions = require("./utils/exceptions");
|
|
66
66
|
var import_useFlowStep = require("./hooks/useFlowStep");
|
|
67
67
|
var import_views = require("./views");
|
|
68
|
+
var import_lazy_helper = require("./lazy-helper");
|
|
68
69
|
var _forceEnabled, _emitter;
|
|
69
70
|
const Panel = import_antd.Collapse.Panel;
|
|
70
71
|
const _FlowSettings = class _FlowSettings {
|
|
@@ -73,10 +74,12 @@ const _FlowSettings = class _FlowSettings {
|
|
|
73
74
|
__publicField(this, "scopes", {});
|
|
74
75
|
__publicField(this, "antdComponentsLoaded", false);
|
|
75
76
|
__publicField(this, "enabled");
|
|
77
|
+
__publicField(this, "engine");
|
|
76
78
|
__privateAdd(this, _forceEnabled, false);
|
|
77
79
|
// 强制启用状态,主要用于设计模式下的强制启用
|
|
78
80
|
__publicField(this, "toolbarItems", []);
|
|
79
81
|
__privateAdd(this, _emitter, new import_emitter.Emitter());
|
|
82
|
+
this.engine = engine;
|
|
80
83
|
this.enabled = false;
|
|
81
84
|
engine.context.defineProperty("flowSettingsEnabled", {
|
|
82
85
|
get: /* @__PURE__ */ __name(() => this.enabled, "get"),
|
|
@@ -215,6 +218,29 @@ const _FlowSettings = class _FlowSettings {
|
|
|
215
218
|
this.components[name] = components[name];
|
|
216
219
|
});
|
|
217
220
|
}
|
|
221
|
+
registerComponentLoaders(loaders) {
|
|
222
|
+
Object.entries(loaders).forEach(([name, loader]) => {
|
|
223
|
+
if (this.components[name]) {
|
|
224
|
+
console.warn(`FlowSettings: Component with name '${name}' is already registered and will be overwritten.`);
|
|
225
|
+
}
|
|
226
|
+
this.components[name] = (0, import_lazy_helper.lazy)(async () => {
|
|
227
|
+
const loaded = await loader();
|
|
228
|
+
if (typeof loaded === "function") {
|
|
229
|
+
return { default: loaded };
|
|
230
|
+
}
|
|
231
|
+
if ((loaded == null ? void 0 : loaded.default) && typeof loaded.default === "function") {
|
|
232
|
+
return { default: loaded.default };
|
|
233
|
+
}
|
|
234
|
+
const namedComponent = loaded == null ? void 0 : loaded[name];
|
|
235
|
+
if (typeof namedComponent === "function") {
|
|
236
|
+
return { default: namedComponent };
|
|
237
|
+
}
|
|
238
|
+
throw new Error(
|
|
239
|
+
`FlowSettings: component loader for '${name}' must resolve to a React component or a module exporting it.`
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
218
244
|
/**
|
|
219
245
|
* 添加作用域到 FlowSettings 的作用域注册表中。
|
|
220
246
|
* 这些作用域可以在 flow step 的 uiSchema 中使用。
|
|
@@ -234,27 +260,29 @@ const _FlowSettings = class _FlowSettings {
|
|
|
234
260
|
/**
|
|
235
261
|
* 启用流程设置组件的显示
|
|
236
262
|
* @example
|
|
237
|
-
* flowSettings.enable();
|
|
263
|
+
* await flowSettings.enable();
|
|
238
264
|
*/
|
|
239
|
-
enable() {
|
|
265
|
+
async enable() {
|
|
266
|
+
await this.engine.preloadModelLoaders();
|
|
240
267
|
this.enabled = true;
|
|
241
268
|
}
|
|
242
|
-
forceEnable() {
|
|
269
|
+
async forceEnable() {
|
|
270
|
+
await this.engine.preloadModelLoaders();
|
|
243
271
|
__privateSet(this, _forceEnabled, true);
|
|
244
272
|
this.enabled = true;
|
|
245
273
|
}
|
|
246
274
|
/**
|
|
247
275
|
* 禁用流程设置组件的显示
|
|
248
276
|
* @example
|
|
249
|
-
* flowSettings.disable();
|
|
277
|
+
* await flowSettings.disable();
|
|
250
278
|
*/
|
|
251
|
-
disable() {
|
|
279
|
+
async disable() {
|
|
252
280
|
if (__privateGet(this, _forceEnabled)) {
|
|
253
281
|
return;
|
|
254
282
|
}
|
|
255
283
|
this.enabled = false;
|
|
256
284
|
}
|
|
257
|
-
forceDisable() {
|
|
285
|
+
async forceDisable() {
|
|
258
286
|
__privateSet(this, _forceEnabled, false);
|
|
259
287
|
this.enabled = false;
|
|
260
288
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
type LazyComponentType<M extends Record<string, any>, K extends keyof M> = {
|
|
10
|
+
[P in K]: M[P];
|
|
11
|
+
};
|
|
12
|
+
export declare function lazy<M extends Record<'default', any>>(factory: () => Promise<M>): M['default'];
|
|
13
|
+
export declare function lazy<M extends Record<string, any>, K extends keyof M = keyof M>(factory: () => Promise<M>, ...componentNames: K[]): LazyComponentType<M, K>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
var __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
var __copyProps = (to, from, except, desc) => {
|
|
22
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
23
|
+
for (let key of __getOwnPropNames(from))
|
|
24
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
25
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
26
|
+
}
|
|
27
|
+
return to;
|
|
28
|
+
};
|
|
29
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
30
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
31
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
32
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
33
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
34
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
35
|
+
mod
|
|
36
|
+
));
|
|
37
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
38
|
+
var lazy_helper_exports = {};
|
|
39
|
+
__export(lazy_helper_exports, {
|
|
40
|
+
lazy: () => lazy
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(lazy_helper_exports);
|
|
43
|
+
var import_react = __toESM(require("react"));
|
|
44
|
+
function lazy(factory, ...componentNames) {
|
|
45
|
+
if (componentNames.length === 0) {
|
|
46
|
+
const LazyComponent = (0, import_react.lazy)(
|
|
47
|
+
() => factory().then((module2) => ({
|
|
48
|
+
default: module2.default
|
|
49
|
+
}))
|
|
50
|
+
);
|
|
51
|
+
const Component = /* @__PURE__ */ __name((props) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Suspense, { fallback: null }, /* @__PURE__ */ import_react.default.createElement(LazyComponent, { ...props })), "Component");
|
|
52
|
+
return Component;
|
|
53
|
+
}
|
|
54
|
+
return componentNames.reduce(
|
|
55
|
+
(acc, name) => {
|
|
56
|
+
const LazyComponent = (0, import_react.lazy)(
|
|
57
|
+
() => factory().then((module2) => ({
|
|
58
|
+
default: module2[name]
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
acc[name] = (props) => /* @__PURE__ */ import_react.default.createElement(import_react.default.Suspense, { fallback: null }, /* @__PURE__ */ import_react.default.createElement(LazyComponent, { ...props }));
|
|
62
|
+
return acc;
|
|
63
|
+
},
|
|
64
|
+
{}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
__name(lazy, "lazy");
|
|
68
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
69
|
+
0 && (module.exports = {
|
|
70
|
+
lazy
|
|
71
|
+
});
|
package/lib/models/flowModel.js
CHANGED
|
@@ -56,13 +56,11 @@ 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");
|
|
66
64
|
var import_ModelActionRegistry = require("../action-registry/ModelActionRegistry");
|
|
67
65
|
var import_utils2 = require("../components/subModel/utils");
|
|
68
66
|
var import_ModelEventRegistry = require("../event-registry/ModelEventRegistry");
|
|
@@ -75,6 +73,16 @@ 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";
|
|
@@ -1058,7 +1066,7 @@ const _FlowModel = class _FlowModel {
|
|
|
1058
1066
|
* @param {string} stepKey 步骤的唯一标识符
|
|
1059
1067
|
* @returns {void}
|
|
1060
1068
|
*/
|
|
1061
|
-
openStepSettingsDialog(flowKey, stepKey) {
|
|
1069
|
+
async openStepSettingsDialog(flowKey, stepKey) {
|
|
1062
1070
|
var _a;
|
|
1063
1071
|
const flow = this.getFlow(flowKey);
|
|
1064
1072
|
const step = (_a = flow == null ? void 0 : flow.steps) == null ? void 0 : _a[stepKey];
|
|
@@ -1069,7 +1077,8 @@ const _FlowModel = class _FlowModel {
|
|
|
1069
1077
|
const ctx = new import_flowContext.FlowRuntimeContext(this, flowKey, "settings");
|
|
1070
1078
|
(0, import_utils.setupRuntimeContextSteps)(ctx, flow.steps, this, flowKey);
|
|
1071
1079
|
ctx.defineProperty("currentStep", { value: step });
|
|
1072
|
-
|
|
1080
|
+
const openStepSettingsDialog = await loadOpenStepSettingsDialog();
|
|
1081
|
+
return openStepSettingsDialog({
|
|
1073
1082
|
model: this,
|
|
1074
1083
|
flowKey,
|
|
1075
1084
|
stepKey,
|
|
@@ -1084,7 +1093,8 @@ const _FlowModel = class _FlowModel {
|
|
|
1084
1093
|
* @returns {Promise<any>} 返回表单提交的值
|
|
1085
1094
|
*/
|
|
1086
1095
|
async configureRequiredSteps(dialogWidth, dialogTitle) {
|
|
1087
|
-
|
|
1096
|
+
const openRequiredParamsStepFormDialog = await loadOpenRequiredParamsStepFormDialog();
|
|
1097
|
+
return openRequiredParamsStepFormDialog({
|
|
1088
1098
|
model: this,
|
|
1089
1099
|
dialogWidth,
|
|
1090
1100
|
dialogTitle
|
|
@@ -1303,7 +1313,7 @@ const _ErrorFlowModel = class _ErrorFlowModel extends FlowModel {
|
|
|
1303
1313
|
this.errorMessage = msg;
|
|
1304
1314
|
}
|
|
1305
1315
|
render() {
|
|
1306
|
-
return /* @__PURE__ */ import_react.default.createElement(
|
|
1316
|
+
return /* @__PURE__ */ import_react.default.createElement(import_antd.Typography.Text, { type: "danger" }, this.errorMessage);
|
|
1307
1317
|
}
|
|
1308
1318
|
};
|
|
1309
1319
|
__name(_ErrorFlowModel, "ErrorFlowModel");
|
package/lib/types.d.ts
CHANGED
|
@@ -303,6 +303,41 @@ 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.
|
|
320
|
+
* Future contribution-style fields are intentionally kept as commented placeholders in phase A
|
|
321
|
+
* and are not consumed by current runtime logic.
|
|
322
|
+
*/
|
|
323
|
+
export interface FlowModelLoaderEntry {
|
|
324
|
+
loader: FlowModelLoader;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* FlowModel loader entry map.
|
|
328
|
+
*/
|
|
329
|
+
export type FlowModelLoaderMap = Record<string, FlowModelLoaderEntry>;
|
|
330
|
+
/**
|
|
331
|
+
* Batch ensure result.
|
|
332
|
+
*/
|
|
333
|
+
export interface EnsureBatchResult {
|
|
334
|
+
requested: string[];
|
|
335
|
+
loaded: string[];
|
|
336
|
+
failed: Array<{
|
|
337
|
+
name: string;
|
|
338
|
+
error?: unknown;
|
|
339
|
+
}>;
|
|
340
|
+
}
|
|
306
341
|
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
|
307
342
|
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
|
308
343
|
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.0.
|
|
3
|
+
"version": "2.1.0-alpha.10",
|
|
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.0.
|
|
12
|
-
"@nocobase/shared": "2.0.
|
|
11
|
+
"@nocobase/sdk": "2.1.0-alpha.10",
|
|
12
|
+
"@nocobase/shared": "2.1.0-alpha.10",
|
|
13
13
|
"ahooks": "^3.7.2",
|
|
14
14
|
"dayjs": "^1.11.9",
|
|
15
15
|
"dompurify": "^3.0.2",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "NocoBase Team",
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "ce790d46c0a5768ca9618c7d0d77ab8300de75c8"
|
|
40
40
|
}
|
|
@@ -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
|
+
});
|