@nocobase/flow-engine 2.0.58 → 2.0.59

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.
@@ -153,7 +153,7 @@ const _FlowExecutor = class _FlowExecutor {
153
153
  const stepDefaultParams = await (0, import_utils.resolveDefaultParams)(step.defaultParams, runtimeCtx);
154
154
  combinedParams = { ...stepDefaultParams };
155
155
  } else {
156
- flowContext.logger.error(
156
+ flowContext.logger.warn(
157
157
  `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`
158
158
  );
159
159
  continue;
package/lib/flowEngine.js CHANGED
@@ -63,6 +63,7 @@ var import_emitter = require("./emitter");
63
63
  var import_ModelOperationScheduler = __toESM(require("./scheduler/ModelOperationScheduler"));
64
64
  var import_utils = require("./utils");
65
65
  var _FlowEngine_instances, registerModel_fn;
66
+ const getFlowEngineLoggerLevel = /* @__PURE__ */ __name(() => process.env.NODE_ENV === "production" ? "warn" : "trace", "getFlowEngineLoggerLevel");
66
67
  const _FlowEngine = class _FlowEngine {
67
68
  /**
68
69
  * Constructor. Initializes React view, registers default model and form scopes.
@@ -173,7 +174,7 @@ const _FlowEngine = class _FlowEngine {
173
174
  MultiRecordResource: import_resources.MultiRecordResource
174
175
  });
175
176
  this.logger = (0, import_pino.default)({
176
- level: "trace",
177
+ level: getFlowEngineLoggerLevel(),
177
178
  browser: {
178
179
  write: {
179
180
  fatal: /* @__PURE__ */ __name((o) => console.trace(o), "fatal"),
@@ -510,7 +511,6 @@ const _FlowEngine = class _FlowEngine {
510
511
  const visited = /* @__PURE__ */ new Set();
511
512
  while (current) {
512
513
  if (visited.has(current)) {
513
- console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
514
514
  break;
515
515
  }
516
516
  visited.add(current);
@@ -611,7 +611,7 @@ const _FlowEngine = class _FlowEngine {
611
611
  removeModel(uid) {
612
612
  var _a, _b, _c;
613
613
  if (!this._modelInstances.has(uid)) {
614
- console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`);
614
+ this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
615
615
  return false;
616
616
  }
617
617
  const modelInstance = this._modelInstances.get(uid);
@@ -624,7 +624,7 @@ const _FlowModel = class _FlowModel {
624
624
  }
625
625
  const isFork = this.isFork === true;
626
626
  const target = this;
627
- console.log(
627
+ currentFlowEngine.logger.debug(
628
628
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${this.cleanRun}, targetIsFork=${(target == null ? void 0 : target.isFork) === true}`
629
629
  );
630
630
  return currentFlowEngine.executor.runFlow(target, flowKey, inputArgs, runId);
@@ -637,7 +637,7 @@ const _FlowModel = class _FlowModel {
637
637
  }
638
638
  const isFork = this.isFork === true;
639
639
  const target = this;
640
- console.log(
640
+ currentFlowEngine.logger.debug(
641
641
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${this.cleanRun}, targetIsFork=${(target == null ? void 0 : target.isFork) === true}`
642
642
  );
643
643
  return await currentFlowEngine.executor.dispatchEvent(target, eventName, inputArgs, options);
@@ -1012,7 +1012,7 @@ const _FlowModel = class _FlowModel {
1012
1012
  }
1013
1013
  clearForks() {
1014
1014
  var _a;
1015
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1015
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1016
1016
  if ((_a = this.forks) == null ? void 0 : _a.size) {
1017
1017
  this.forks.forEach((fork) => fork.dispose());
1018
1018
  this.forks.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.58",
3
+ "version": "2.0.59",
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.58",
12
- "@nocobase/shared": "2.0.58",
11
+ "@nocobase/sdk": "2.0.59",
12
+ "@nocobase/shared": "2.0.59",
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": "158b99ec4f71555fc5125f61cfcf357763994da0"
40
+ "gitHead": "8b3a0cfadf6cb2da4a7e9213ef0d8d092d885c4a"
41
41
  }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { beforeEach, describe, expect, it } from 'vitest';
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
11
11
  import { FlowEngine } from '../flowEngine';
12
12
  import { FlowModel } from '../models';
13
13
 
@@ -20,6 +20,7 @@ describe('FlowEngine removeModel', () => {
20
20
  });
21
21
 
22
22
  it('removeModel should remove model but keep sub-models in cache (current behavior)', () => {
23
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
23
24
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
24
25
  const child = engine.createModel({
25
26
  uid: 'child',
@@ -32,14 +33,53 @@ describe('FlowEngine removeModel', () => {
32
33
  expect(engine.getModel('parent')).toBe(parent);
33
34
  expect(engine.getModel('child')).toBe(child);
34
35
 
35
- engine.removeModel('parent');
36
+ try {
37
+ engine.removeModel('parent');
38
+ } finally {
39
+ loggerSpy.mockRestore();
40
+ }
36
41
 
37
42
  expect(engine.getModel('parent')).toBeUndefined();
38
43
  // Current behavior: child is still in cache
39
44
  expect(engine.getModel('child')).toBeDefined();
40
45
  });
41
46
 
47
+ it('removeModel should log missing models at debug level', () => {
48
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
49
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
50
+
51
+ try {
52
+ expect(engine.removeModel('missing')).toBe(false);
53
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
54
+ expect(loggerSpy).toHaveBeenCalledWith("FlowEngine: Model with UID 'missing' does not exist.");
55
+ } finally {
56
+ consoleWarnSpy.mockRestore();
57
+ loggerSpy.mockRestore();
58
+ }
59
+ });
60
+
61
+ it('should reduce default logger verbosity in production', () => {
62
+ const originalNodeEnv = process.env.NODE_ENV;
63
+
64
+ try {
65
+ process.env.NODE_ENV = 'production';
66
+
67
+ const productionEngine = new FlowEngine();
68
+
69
+ expect(productionEngine.logger.level).toBe('warn');
70
+ expect(productionEngine.logger.isLevelEnabled('debug')).toBe(false);
71
+ expect(productionEngine.logger.isLevelEnabled('warn')).toBe(true);
72
+ } finally {
73
+ if (originalNodeEnv === undefined) {
74
+ delete process.env.NODE_ENV;
75
+ } else {
76
+ process.env.NODE_ENV = originalNodeEnv;
77
+ }
78
+ }
79
+ });
80
+
42
81
  it('removeModelWithSubModels should remove model and all sub-models from cache', () => {
82
+ const loggerSpy = vi.spyOn(engine.logger, 'debug').mockImplementation(() => {});
43
83
  const parent = engine.createModel({ uid: 'parent', use: 'FlowModel' });
44
84
  const child = engine.createModel({
45
85
  uid: 'child',
@@ -63,7 +103,11 @@ describe('FlowEngine removeModel', () => {
63
103
  expect(engine.getModel('child')).toBe(child);
64
104
  expect(engine.getModel('grandChild')).toBe(grandChild);
65
105
 
66
- engine.removeModelWithSubModels('parent');
106
+ try {
107
+ engine.removeModelWithSubModels('parent');
108
+ } finally {
109
+ loggerSpy.mockRestore();
110
+ }
67
111
 
68
112
  expect(engine.getModel('parent')).toBeUndefined();
69
113
  expect(engine.getModel('child')).toBeUndefined();
@@ -158,7 +158,7 @@ export class FlowExecutor {
158
158
  const stepDefaultParams = await resolveDefaultParams(step.defaultParams, runtimeCtx);
159
159
  combinedParams = { ...stepDefaultParams };
160
160
  } else {
161
- flowContext.logger.error(
161
+ flowContext.logger.warn(
162
162
  `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`,
163
163
  );
164
164
  continue;
@@ -81,6 +81,34 @@ describe('FlowExecutor', () => {
81
81
  expect(result.step2).toBe('step2-ok');
82
82
  });
83
83
 
84
+ it('runFlow warns and skips steps without use or handler', async () => {
85
+ const flows = {
86
+ referenceSettings: {
87
+ steps: {
88
+ target: {},
89
+ },
90
+ },
91
+ } satisfies Record<string, Omit<FlowDefinitionOptions, 'key'>>;
92
+ const model = createModelWithFlows('m-empty-step', flows);
93
+ const loggerChildSpy = vi.spyOn(engine.logger, 'child').mockReturnValue(engine.logger);
94
+ const loggerWarnSpy = vi.spyOn(engine.logger, 'warn').mockImplementation(() => {});
95
+ const loggerErrorSpy = vi.spyOn(engine.logger, 'error').mockImplementation(() => {});
96
+
97
+ try {
98
+ const result = await engine.executor.runFlow(model, 'referenceSettings');
99
+
100
+ expect(result).toEqual({});
101
+ expect(loggerWarnSpy).toHaveBeenCalledWith(
102
+ "BaseModel.applyFlow: Step 'target' in flow 'referenceSettings' has neither 'use' nor 'handler'. Skipping.",
103
+ );
104
+ expect(loggerErrorSpy).not.toHaveBeenCalled();
105
+ } finally {
106
+ loggerChildSpy.mockRestore();
107
+ loggerWarnSpy.mockRestore();
108
+ loggerErrorSpy.mockRestore();
109
+ }
110
+ });
111
+
84
112
  it("dispatchEvent('beforeRender') executes flows in sort order and caches result (when options specify)", async () => {
85
113
  const calls: string[] = [];
86
114
  const mkFlow = (key: string, sort: number) => ({
package/src/flowEngine.ts CHANGED
@@ -35,6 +35,8 @@ import type {
35
35
  } from './types';
36
36
  import { isInheritedFrom } from './utils';
37
37
 
38
+ const getFlowEngineLoggerLevel = () => (process.env.NODE_ENV === 'production' ? 'warn' : 'trace');
39
+
38
40
  /**
39
41
  * FlowEngine is the core class of the flow engine, responsible for managing flow models, actions, model repository, and more.
40
42
  * It provides capabilities for registering, creating, finding, persisting, replacing, and moving models.
@@ -183,7 +185,7 @@ export class FlowEngine {
183
185
  MultiRecordResource,
184
186
  });
185
187
  this.logger = pino({
186
- level: 'trace',
188
+ level: getFlowEngineLoggerLevel(),
187
189
  browser: {
188
190
  write: {
189
191
  fatal: (o) => console.trace(o),
@@ -608,7 +610,6 @@ export class FlowEngine {
608
610
 
609
611
  while (current) {
610
612
  if (visited.has(current)) {
611
- console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
612
613
  break;
613
614
  }
614
615
  visited.add(current);
@@ -727,7 +728,7 @@ export class FlowEngine {
727
728
  */
728
729
  public removeModel(uid: string): boolean {
729
730
  if (!this._modelInstances.has(uid)) {
730
- console.warn(`FlowEngine: Model with UID '${uid}' does not exist.`);
731
+ this.logger.debug(`FlowEngine: Model with UID '${uid}' does not exist.`);
731
732
  return false;
732
733
  }
733
734
  const modelInstance = this._modelInstances.get(uid) as FlowModel;
@@ -61,21 +61,6 @@ describe('FlowEngine.createModel resolveUse hook', () => {
61
61
  expect(warnSpy).not.toHaveBeenCalled();
62
62
  });
63
63
 
64
- test('should break resolveUse on circular reference and warn', () => {
65
- class LoopModel extends FlowModel {
66
- static resolveUse() {
67
- return 'LoopModel';
68
- }
69
- }
70
-
71
- engine.registerModels({ LoopModel });
72
-
73
- const model = engine.createModel({ use: 'LoopModel', uid: 'loop-model', flowEngine: engine });
74
-
75
- expect(model).toBeInstanceOf(LoopModel);
76
- expect(warnSpy).toHaveBeenCalled();
77
- });
78
-
79
64
  test('should fall back to ErrorFlowModel when resolveUse returns unregistered name', () => {
80
65
  class MissingTargetEntry extends FlowModel {
81
66
  static resolveUse() {
@@ -370,15 +370,17 @@ describe('FlowModel', () => {
370
370
  };
371
371
 
372
372
  TestFlowModel.registerFlow(exitFlow);
373
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
373
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
374
374
 
375
- const result = await model.applyFlow('exitFlow');
376
-
377
- expect(result).toBeInstanceOf(FlowExitAllException);
378
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
379
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
375
+ try {
376
+ const result = await model.applyFlow('exitFlow');
380
377
 
381
- consoleSpy.mockRestore();
378
+ expect(result).toBeInstanceOf(FlowExitAllException);
379
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
380
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
381
+ } finally {
382
+ loggerSpy.mockRestore();
383
+ }
382
384
  });
383
385
 
384
386
  test('should handle ctx.exit() as FlowExitAllException in beforeRender dispatch', async () => {
@@ -474,15 +476,17 @@ describe('FlowModel', () => {
474
476
  };
475
477
 
476
478
  TestFlowModel.registerFlow(exitFlow);
477
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
478
-
479
- const result = await model.applyFlow('exitFlow');
479
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
480
480
 
481
- expect(result).toBeInstanceOf(FlowExitAllException);
482
- expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
483
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
481
+ try {
482
+ const result = await model.applyFlow('exitFlow');
484
483
 
485
- consoleSpy.mockRestore();
484
+ expect(result).toBeInstanceOf(FlowExitAllException);
485
+ expect(exitFlow.steps.step2.handler).not.toHaveBeenCalled();
486
+ expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('[FlowModel]'));
487
+ } finally {
488
+ loggerSpy.mockRestore();
489
+ }
486
490
  });
487
491
 
488
492
  test('should propagate step execution errors', async () => {
@@ -768,7 +772,7 @@ describe('FlowModel', () => {
768
772
  const eventFlow = createEventFlowDefinition('testEvent');
769
773
  TestFlowModel.registerFlow(eventFlow);
770
774
 
771
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
775
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
772
776
 
773
777
  try {
774
778
  model.dispatchEvent('testEvent', { data: 'payload' });
@@ -776,7 +780,7 @@ describe('FlowModel', () => {
776
780
  // Use a more reliable approach than arbitrary timeout
777
781
  await new Promise((resolve) => setTimeout(resolve, 0));
778
782
 
779
- expect(consoleSpy).toHaveBeenCalledWith(
783
+ expect(loggerSpy).toHaveBeenCalledWith(
780
784
  expect.stringContaining('[FlowModel] dispatchEvent: uid=test-model-uid, event=testEvent'),
781
785
  );
782
786
  expect(eventFlow.steps.eventStep.handler).toHaveBeenCalledWith(
@@ -786,7 +790,7 @@ describe('FlowModel', () => {
786
790
  expect.any(Object),
787
791
  );
788
792
  } finally {
789
- consoleSpy.mockRestore();
793
+ loggerSpy.mockRestore();
790
794
  }
791
795
  });
792
796
 
@@ -1597,7 +1601,7 @@ describe('FlowModel', () => {
1597
1601
  fork1.dispose = vi.fn();
1598
1602
  fork2.dispose = vi.fn();
1599
1603
 
1600
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1604
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1601
1605
 
1602
1606
  try {
1603
1607
  model.clearForks();
@@ -1606,19 +1610,19 @@ describe('FlowModel', () => {
1606
1610
  expect(fork2.dispose).toHaveBeenCalled();
1607
1611
  expect(model.forks.size).toBe(0);
1608
1612
  } finally {
1609
- consoleSpy.mockRestore();
1613
+ loggerSpy.mockRestore();
1610
1614
  }
1611
1615
  });
1612
1616
 
1613
1617
  test('should handle empty forks collection when clearing', () => {
1614
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1618
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1615
1619
 
1616
1620
  try {
1617
1621
  model.clearForks();
1618
1622
 
1619
1623
  expect(model.forks.size).toBe(0);
1620
1624
  } finally {
1621
- consoleSpy.mockRestore();
1625
+ loggerSpy.mockRestore();
1622
1626
  }
1623
1627
  });
1624
1628
  });
@@ -1746,7 +1750,7 @@ describe('FlowModel', () => {
1746
1750
  test('should clean up resources on remove', () => {
1747
1751
  model.createFork();
1748
1752
  model.createFork();
1749
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1753
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
1750
1754
 
1751
1755
  // Mock removeModel to simulate proper fork cleanup
1752
1756
  flowEngine.removeModel = vi.fn().mockImplementation(() => {
@@ -1763,7 +1767,7 @@ describe('FlowModel', () => {
1763
1767
  expect(model.forks.size).toBe(0);
1764
1768
  expect(flowEngine.removeModel).toHaveBeenCalledWith(model.uid);
1765
1769
  } finally {
1766
- consoleSpy.mockRestore();
1770
+ loggerSpy.mockRestore();
1767
1771
  }
1768
1772
  });
1769
1773
  });
@@ -1840,17 +1844,12 @@ describe('FlowModel', () => {
1840
1844
  });
1841
1845
 
1842
1846
  test('should rerender triggers beforeRender without cache', async () => {
1843
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
1844
1847
  model.dispatchEvent = vi.fn().mockResolvedValue(undefined) as any;
1845
1848
 
1846
- try {
1847
- await expect(model.rerender()).resolves.not.toThrow();
1848
- expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1849
- useCache: false,
1850
- });
1851
- } finally {
1852
- consoleSpy.mockRestore();
1853
- }
1849
+ await expect(model.rerender()).resolves.not.toThrow();
1850
+ expect(model.dispatchEvent).toHaveBeenCalledWith('beforeRender', undefined, {
1851
+ useCache: false,
1852
+ });
1854
1853
  });
1855
1854
  });
1856
1855
 
@@ -2874,7 +2873,7 @@ describe('FlowModel', () => {
2874
2873
  describe('Edge Cases & Error Handling', () => {
2875
2874
  test('should handle model destruction gracefully', () => {
2876
2875
  const model = new FlowModel(modelOptions);
2877
- const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
2876
+ const loggerSpy = vi.spyOn(model.flowEngine.logger, 'debug').mockImplementation(() => {});
2878
2877
 
2879
2878
  model.createFork();
2880
2879
  model.setProps({ testProp: 'value' });
@@ -2882,7 +2881,7 @@ describe('FlowModel', () => {
2882
2881
  try {
2883
2882
  expect(() => model.remove()).not.toThrow();
2884
2883
  } finally {
2885
- consoleSpy.mockRestore();
2884
+ loggerSpy.mockRestore();
2886
2885
  }
2887
2886
  });
2888
2887
 
@@ -757,7 +757,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
757
757
  }
758
758
  const isFork = (this as any).isFork === true;
759
759
  const target = this;
760
- console.log(
760
+ currentFlowEngine.logger.debug(
761
761
  `[FlowModel] applyFlow: uid=${this.uid}, flowKey=${flowKey}, isFork=${isFork}, cleanRun=${
762
762
  this.cleanRun
763
763
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -777,7 +777,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
777
777
  }
778
778
  const isFork = (this as any).isFork === true;
779
779
  const target = this;
780
- console.log(
780
+ currentFlowEngine.logger.debug(
781
781
  `[FlowModel] dispatchEvent: uid=${this.uid}, event=${eventName}, isFork=${isFork}, cleanRun=${
782
782
  this.cleanRun
783
783
  }, targetIsFork=${(target as any)?.isFork === true}`,
@@ -1313,7 +1313,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1313
1313
  }
1314
1314
 
1315
1315
  clearForks() {
1316
- console.log(`FlowModel ${this.uid} clearing all forks.`);
1316
+ this.flowEngine.logger.debug(`FlowModel ${this.uid} clearing all forks.`);
1317
1317
  // 主动使所有 fork 失效
1318
1318
  if (this.forks?.size) {
1319
1319
  this.forks.forEach((fork) => fork.dispose());