@rytass/bpm-core-nestjs-module 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +3 -2
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -0
  5. package/src/index.js.map +1 -1
  6. package/src/lib/bpm/bpm-root-options.d.ts +13 -0
  7. package/src/lib/bpm/bpm-root-options.js.map +1 -1
  8. package/src/lib/bpm/bpm-root.module.d.ts +12 -0
  9. package/src/lib/bpm/bpm-root.module.js +5 -0
  10. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  11. package/src/lib/condition/adapters/cel-legacy.adapter.d.ts +62 -0
  12. package/src/lib/condition/adapters/cel-legacy.adapter.js +141 -0
  13. package/src/lib/condition/adapters/cel-legacy.adapter.js.map +1 -0
  14. package/src/lib/condition/adapters/cel-v2.adapter.d.ts +76 -0
  15. package/src/lib/condition/adapters/cel-v2.adapter.js +371 -0
  16. package/src/lib/condition/adapters/cel-v2.adapter.js.map +1 -0
  17. package/src/lib/condition/condition.service.d.ts +90 -5
  18. package/src/lib/condition/condition.service.js +287 -71
  19. package/src/lib/condition/condition.service.js.map +1 -1
  20. package/src/lib/condition/expression-ast.d.ts +176 -0
  21. package/src/lib/condition/expression-ast.js +1254 -0
  22. package/src/lib/condition/expression-ast.js.map +1 -0
  23. package/src/lib/condition/expression-budget.d.ts +44 -0
  24. package/src/lib/condition/expression-budget.js +61 -0
  25. package/src/lib/condition/expression-budget.js.map +1 -0
  26. package/src/lib/condition/expression-dialect.d.ts +29 -0
  27. package/src/lib/condition/expression-dialect.js +44 -0
  28. package/src/lib/condition/expression-dialect.js.map +1 -0
  29. package/src/lib/condition/expression-differential.corpus.d.ts +34 -0
  30. package/src/lib/condition/expression-differential.corpus.js +995 -0
  31. package/src/lib/condition/expression-differential.corpus.js.map +1 -0
  32. package/src/lib/condition/expression-differential.d.ts +33 -0
  33. package/src/lib/condition/expression-differential.js +48 -0
  34. package/src/lib/condition/expression-differential.js.map +1 -0
  35. package/src/lib/condition/expression-engine-failure.d.ts +15 -0
  36. package/src/lib/condition/expression-engine-failure.js +22 -0
  37. package/src/lib/condition/expression-engine-failure.js.map +1 -0
  38. package/src/lib/condition/expression-errors.d.ts +40 -0
  39. package/src/lib/condition/expression-errors.js +121 -0
  40. package/src/lib/condition/expression-errors.js.map +1 -0
  41. package/src/lib/condition/expression-function-invocation.d.ts +50 -0
  42. package/src/lib/condition/expression-function-invocation.js +290 -0
  43. package/src/lib/condition/expression-function-invocation.js.map +1 -0
  44. package/src/lib/condition/expression-result.walker.d.ts +18 -0
  45. package/src/lib/condition/expression-result.walker.js +152 -0
  46. package/src/lib/condition/expression-result.walker.js.map +1 -0
  47. package/src/lib/condition/expression-runtime.d.ts +70 -0
  48. package/src/lib/condition/expression-runtime.js +134 -0
  49. package/src/lib/condition/expression-runtime.js.map +1 -0
  50. package/src/lib/condition/index.d.ts +4 -0
  51. package/src/lib/condition/index.js +8 -0
  52. package/src/lib/condition/index.js.map +1 -1
  53. package/src/lib/delegation/delegation-rule.entity.d.ts +7 -0
  54. package/src/lib/delegation/delegation-rule.entity.js +10 -0
  55. package/src/lib/delegation/delegation-rule.entity.js.map +1 -1
  56. package/src/lib/delegation/delegation.service.d.ts +7 -1
  57. package/src/lib/delegation/delegation.service.js +22 -5
  58. package/src/lib/delegation/delegation.service.js.map +1 -1
  59. package/src/lib/delegation/dto/delegation-rule.input.d.ts +2 -0
  60. package/src/lib/delegation/dto/delegation-rule.input.js +10 -0
  61. package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -1
  62. package/src/lib/expression-function/expression-function.built-ins.d.ts +9 -0
  63. package/src/lib/expression-function/expression-function.built-ins.js +117 -0
  64. package/src/lib/expression-function/expression-function.built-ins.js.map +1 -0
  65. package/src/lib/expression-function/expression-function.module.d.ts +19 -0
  66. package/src/lib/expression-function/expression-function.module.js +34 -0
  67. package/src/lib/expression-function/expression-function.module.js.map +1 -0
  68. package/src/lib/expression-function/expression-function.provider.d.ts +17 -0
  69. package/src/lib/expression-function/expression-function.provider.js +39 -0
  70. package/src/lib/expression-function/expression-function.provider.js.map +1 -0
  71. package/src/lib/expression-function/expression-function.registry.d.ts +36 -0
  72. package/src/lib/expression-function/expression-function.registry.js +231 -0
  73. package/src/lib/expression-function/expression-function.registry.js.map +1 -0
  74. package/src/lib/expression-function/expression-function.types.d.ts +30 -0
  75. package/src/lib/expression-function/expression-function.types.js +8 -0
  76. package/src/lib/expression-function/expression-function.types.js.map +1 -0
  77. package/src/lib/expression-function/index.d.ts +5 -0
  78. package/src/lib/expression-function/index.js +9 -0
  79. package/src/lib/expression-function/index.js.map +1 -0
  80. package/src/lib/form/form-schema.validator.js +53 -6
  81. package/src/lib/form/form-schema.validator.js.map +1 -1
  82. package/src/lib/form-data-source/form-data-source-value-resolver.service.d.ts +12 -1
  83. package/src/lib/form-data-source/form-data-source-value-resolver.service.js +45 -26
  84. package/src/lib/form-data-source/form-data-source-value-resolver.service.js.map +1 -1
  85. package/src/lib/form-data-source/form-data-source.expression.d.ts +95 -0
  86. package/src/lib/form-data-source/form-data-source.expression.js +267 -0
  87. package/src/lib/form-data-source/form-data-source.expression.js.map +1 -0
  88. package/src/lib/form-data-source/form-data-source.module.js +4 -0
  89. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  90. package/src/lib/form-data-source/form-data-source.service.d.ts +22 -3
  91. package/src/lib/form-data-source/form-data-source.service.js +78 -18
  92. package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
  93. package/src/lib/form-data-source/form-data-source.validation.d.ts +47 -4
  94. package/src/lib/form-data-source/form-data-source.validation.js +151 -23
  95. package/src/lib/form-data-source/form-data-source.validation.js.map +1 -1
  96. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.d.ts +22 -0
  97. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js +76 -0
  98. package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js.map +1 -0
  99. package/src/lib/migrations/index.d.ts +1 -0
  100. package/src/lib/migrations/index.js +3 -0
  101. package/src/lib/migrations/index.js.map +1 -1
  102. package/src/lib/notification/notification.service.d.ts +1 -1
  103. package/src/lib/notification/notification.service.js +7 -3
  104. package/src/lib/notification/notification.service.js.map +1 -1
  105. package/src/lib/template/dto/expression-designer.dto.d.ts +107 -0
  106. package/src/lib/template/dto/expression-designer.dto.js +415 -0
  107. package/src/lib/template/dto/expression-designer.dto.js.map +1 -0
  108. package/src/lib/template/expression-designer.queries.d.ts +16 -0
  109. package/src/lib/template/expression-designer.queries.js +66 -0
  110. package/src/lib/template/expression-designer.queries.js.map +1 -0
  111. package/src/lib/template/expression-designer.service.d.ts +72 -0
  112. package/src/lib/template/expression-designer.service.js +543 -0
  113. package/src/lib/template/expression-designer.service.js.map +1 -0
  114. package/src/lib/template/index.d.ts +2 -0
  115. package/src/lib/template/index.js +2 -0
  116. package/src/lib/template/index.js.map +1 -1
  117. package/src/lib/template/template.module.js +4 -0
  118. package/src/lib/template/template.module.js.map +1 -1
  119. package/src/lib/template/template.service.d.ts +37 -1
  120. package/src/lib/template/template.service.js +317 -29
  121. package/src/lib/template/template.service.js.map +1 -1
  122. package/src/lib/template/workflow-definition.validator.d.ts +7 -0
  123. package/src/lib/template/workflow-definition.validator.js +26 -2
  124. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  125. package/src/lib/workflow-engine/time-budget.d.ts +13 -0
  126. package/src/lib/workflow-engine/time-budget.js +58 -0
  127. package/src/lib/workflow-engine/time-budget.js.map +1 -0
  128. package/src/lib/workflow-engine/workflow-condition-evaluator.js +12 -12
  129. package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -1
  130. package/src/lib/workflow-engine/workflow-engine.service.d.ts +24 -0
  131. package/src/lib/workflow-engine/workflow-engine.service.js +311 -88
  132. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  133. package/src/lib/workflow-engine/workflow-form-value.d.ts +11 -0
  134. package/src/lib/workflow-engine/workflow-form-value.js +33 -0
  135. package/src/lib/workflow-engine/workflow-form-value.js.map +1 -0
  136. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +4 -0
  137. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -1
  138. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +3 -1
  139. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +11 -2
  140. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -1
  141. package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +8 -1
  142. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +70 -27
  143. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -1
  144. package/src/lib/workflow-webhook/workflow-webhook-expression.d.ts +57 -0
  145. package/src/lib/workflow-webhook/workflow-webhook-expression.js +123 -0
  146. package/src/lib/workflow-webhook/workflow-webhook-expression.js.map +1 -0
  147. package/src/lib/workflow-webhook/workflow-webhook.module.js +4 -0
  148. package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
  149. package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +7 -0
  150. package/src/lib/workflow-webhook/workflow-webhook.validator.js +29 -6
  151. package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -1
@@ -47,10 +47,19 @@ const task_candidate_entity_1 = require("./task-candidate.entity");
47
47
  const task_entity_1 = require("./task.entity");
48
48
  const workflow_engine_enums_1 = require("./workflow-engine.enums");
49
49
  const workflow_condition_evaluator_1 = require("./workflow-condition-evaluator");
50
+ const workflow_form_value_1 = require("./workflow-form-value");
50
51
  const workflow_dry_run_object_1 = require("./workflow-dry-run.object");
52
+ const time_budget_1 = require("./time-budget");
53
+ const expression_budget_1 = require("../condition/expression-budget");
54
+ const expression_errors_1 = require("../condition/expression-errors");
55
+ const expression_dialect_1 = require("../condition/expression-dialect");
51
56
  const workflow_service_task_dispatcher_token_1 = require("./workflow-service-task-dispatcher.token");
52
57
  const workflow_token_entity_1 = require("./workflow-token.entity");
53
58
  const bpm_root_options_1 = require("../bpm/bpm-root-options");
59
+ const MAX_DRY_RUN_INPUT_LENGTH = 1_048_576;
60
+ const MAX_DRY_RUN_VISITS = 10_000;
61
+ const DRY_RUN_TIME_BUDGET_MS = 2_000;
62
+ const DRY_RUN_TOO_COMPLEX_MESSAGE = '流程或條件的運算量過大,無法完成試跑,請減少連續的閘道分支或簡化條件';
54
63
  const MAX_PROCESSING_STEPS = 500;
55
64
  exports.MAX_APPROVAL_INSTANCE_BUSINESS_KEY_LENGTH = 200;
56
65
  const APPROVAL_INSTANCE_BUSINESS_KEY_INDEX = 'approval_instances_business_key_key';
@@ -58,6 +67,40 @@ const APPROVAL_INSTANCE_BUSINESS_KEY_INDEX = 'approval_instances_business_key_ke
58
67
  const ORG_MANAGER_CHAIN_MAX_HOPS = 20;
59
68
  const DEFAULT_APPROVAL_HISTORY_TASK_LIMIT = 50;
60
69
  const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
70
+ // The dry run checks each expression against the roots its use point may read,
71
+ // as publish does. Keep these in step with `WORKFLOW_EXPRESSION_ROOT_IDENTIFIERS`
72
+ // in template.service.ts; P2 moves both to one place.
73
+ const DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS = [
74
+ 'env',
75
+ 'form',
76
+ 'formData',
77
+ 'initiator',
78
+ 'instance',
79
+ ];
80
+ const DRY_RUN_EDGE_ROOT_IDENTIFIERS = [
81
+ ...DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
82
+ 'lastDecision',
83
+ ];
84
+ const hasExpressionText = (expression) => typeof expression === 'string' && expression.trim() !== '';
85
+ /**
86
+ * Whether a definition's dialect can be read: missing (`cel-legacy`) or one
87
+ * this version knows. Never throws, unlike `readWorkflowExpressionDialect`.
88
+ */
89
+ const isReadableExpressionDialect = (definition) => {
90
+ const dialect = definition?.expressionDialect;
91
+ return (dialect === undefined || dialect === null || (0, expression_dialect_1.isExpressionDialect)(dialect));
92
+ };
93
+ /**
94
+ * Options for evaluating an expression that may be blank. The dialect is read
95
+ * only when there is an expression: an unknown dialect is refused because the
96
+ * expression cannot be evaluated as its author tested it, and a blank one is
97
+ * never evaluated at all. Publishing does not check the field yet (Phase 23
98
+ * P2), so reading it eagerly would turn a stray field on a version with no
99
+ * expressions into an error, where before dialects existed it was ignored.
100
+ */
101
+ const readExpressionDialectOptions = (expression, definition) => hasExpressionText(expression)
102
+ ? { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(definition) }
103
+ : {};
61
104
  /**
62
105
  * Delegation-chain reason recorded for an approver-initiated transfer, as
63
106
  * opposed to {@link SLA_ESCALATION_DELEGATION_REASON}.
@@ -152,6 +195,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
152
195
  this.formDataSourceValueResolver = formDataSourceValueResolver;
153
196
  this.webhookDeliveryService = webhookDeliveryService;
154
197
  this.logger = new common_1.Logger(WorkflowEngineService_1.name);
198
+ /** Webhooks each open engine transaction will send once it has committed. */
199
+ this.deferredWebhookDispatches = new WeakMap();
155
200
  // A binding under the token wins: that is either the host's own provider
156
201
  // or `workflowServiceTaskDispatcherProvider`, both of them deliberate. The
157
202
  // runtime instance from `BPMRootModule`'s async factory comes next.
@@ -177,6 +222,59 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
177
222
  readOrganizationToday() {
178
223
  return (0, organization_time_zone_1.formatDateOnlyInTimeZone)(new Date(), this.organizationTimeZone);
179
224
  }
225
+ /**
226
+ * Opens an engine transaction whose webhooks wait for its commit. A webhook
227
+ * sent from inside would hold the case lock and a pooled connection for as
228
+ * long as the far end takes to answer, so a slow endpoint could stall the
229
+ * case and then, a connection at a time, the whole API. A transaction that
230
+ * rolls back sends nothing.
231
+ *
232
+ * The caller still waits for the sends, so the outcome is in the activity log
233
+ * by the time a mutation returns, as it always has been.
234
+ */
235
+ async runEngineTransaction(work) {
236
+ // Mutable on purpose: every step of the transaction appends to it, and
237
+ // threading it through each of them would change a dozen signatures.
238
+ const dispatches = [];
239
+ // One transaction is one operation: its evaluations share one budget.
240
+ const result = await (0, expression_budget_1.runExpressionOperation)(() => this.approvalInstanceRepository.manager.transaction((manager) => {
241
+ this.deferredWebhookDispatches.set(manager, dispatches);
242
+ return work(manager);
243
+ }));
244
+ await dispatches.reduce(async (previous, dispatch) => {
245
+ await previous;
246
+ await this.sendCommittedWebhook(dispatch);
247
+ }, Promise.resolve());
248
+ return result;
249
+ }
250
+ /**
251
+ * Queues a webhook for after the commit. A manager this service did not open
252
+ * has no queue and no commit it can see, so that webhook goes out on the spot
253
+ * rather than being dropped.
254
+ */
255
+ async dispatchWebhookAfterCommit(manager, dispatch) {
256
+ const queue = this.deferredWebhookDispatches.get(manager);
257
+ if (queue) {
258
+ queue.push(dispatch);
259
+ return;
260
+ }
261
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
262
+ const result = await dispatch.send();
263
+ await activityRepository.save(activityRepository.create(dispatch.readActivityLog(result)));
264
+ }
265
+ /**
266
+ * The transaction has committed and the mutation has succeeded, so nothing
267
+ * here may fail it: a result that cannot be recorded is only logged.
268
+ */
269
+ async sendCommittedWebhook(dispatch) {
270
+ try {
271
+ const result = await dispatch.send();
272
+ await this.activityLogRepository.save(this.activityLogRepository.create(dispatch.readActivityLog(result)));
273
+ }
274
+ catch (error) {
275
+ this.logger.error(`Could not record a webhook result for instance ${dispatch.instanceId}: ${error instanceof Error ? error.message : String(error)}`);
276
+ }
277
+ }
180
278
  async submitApprovalInstance(input, authContext) {
181
279
  if (input.submittedByMemberId !== undefined &&
182
280
  input.submittedByMemberId !== null &&
@@ -219,7 +317,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
219
317
  const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
220
318
  ? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
221
319
  : await this.readDefaultInitiatorMetadataSnapshot(input.initiatorMemberId);
222
- if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel')) {
320
+ if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel',
321
+ // The policy is written in its version's dialect (ADR 22 §3.3).
322
+ readExpressionDialectOptions(templateVersion.initiatorPolicyCel, templateVersion.workflowDefinition))) {
223
323
  throw new common_1.ConflictException('Current member is not allowed to start this approval template');
224
324
  }
225
325
  // Named here, before the transaction, so the completion notice that later
@@ -233,7 +333,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
233
333
  if (!startNode) {
234
334
  throw new common_1.BadRequestException('Published workflow does not include a startEvent');
235
335
  }
236
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
336
+ return this.runEngineTransaction(async (manager) => {
237
337
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
238
338
  const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
239
339
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
@@ -370,7 +470,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
370
470
  return { formDataOptionSnapshot, instance };
371
471
  }
372
472
  async processInstance(instanceId) {
373
- await this.approvalInstanceRepository.manager.transaction(async (manager) => {
473
+ await this.runEngineTransaction(async (manager) => {
374
474
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
375
475
  instanceId,
376
476
  ]);
@@ -397,7 +497,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
397
497
  });
398
498
  }
399
499
  async decideTask(input, options = {}) {
400
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
500
+ return this.runEngineTransaction(async (manager) => {
401
501
  const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
402
502
  const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
403
503
  const taskDecisionRepository = manager.getRepository(task_decision_entity_1.TaskDecisionEntity);
@@ -760,7 +860,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
760
860
  });
761
861
  }
762
862
  async cancelApprovalInstance(input) {
763
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
863
+ return this.runEngineTransaction(async (manager) => {
764
864
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
765
865
  input.instanceId,
766
866
  ]);
@@ -801,7 +901,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
801
901
  * returned unchanged.
802
902
  */
803
903
  async expireApprovalInstance(input) {
804
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
904
+ return this.runEngineTransaction(async (manager) => {
805
905
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
806
906
  input.instanceId,
807
907
  ]);
@@ -858,7 +958,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
858
958
  const preparation = this.formDataSourceValueResolver
859
959
  ? await this.prepareResubmitFormDataOptions(input, submittedFormData, authContext ?? createWorkflowAuthContext(input.initiatorMemberId))
860
960
  : null;
861
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
961
+ return this.runEngineTransaction(async (manager) => {
862
962
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
863
963
  input.instanceId,
864
964
  ]);
@@ -947,7 +1047,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
947
1047
  });
948
1048
  }
949
1049
  async requestAdhocCountersign({ comment, requestedByMemberId, target, taskId, }) {
950
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1050
+ return this.runEngineTransaction(async (manager) => {
951
1051
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
952
1052
  const targetValue = buildAdhocTargetValue(target);
953
1053
  // Rejects WEBHOOK targets — countersigners must be members.
@@ -979,7 +1079,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
979
1079
  });
980
1080
  }
981
1081
  async requestAdhocPreApproval({ comment, onReject, requestedByMemberId, target, taskId, }) {
982
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1082
+ return this.runEngineTransaction(async (manager) => {
983
1083
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
984
1084
  const targetValue = buildAdhocTargetValue(target);
985
1085
  // Rejects WEBHOOK targets — pre-approvers must be members.
@@ -1006,7 +1106,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1006
1106
  });
1007
1107
  }
1008
1108
  async configureAdhocNotification({ channels, requestedByMemberId, target, taskId, type, }) {
1009
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1109
+ return this.runEngineTransaction(async (manager) => {
1010
1110
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: false });
1011
1111
  const targetValue = buildAdhocTargetValue(target);
1012
1112
  const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
@@ -1029,7 +1129,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1029
1129
  });
1030
1130
  }
1031
1131
  async cancelAdhocDirective({ cancelledByMemberId, directiveId, }) {
1032
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1132
+ return this.runEngineTransaction(async (manager) => {
1033
1133
  const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
1034
1134
  const directive = await directiveRepository.findOne({
1035
1135
  where: { id: directiveId },
@@ -1593,30 +1693,34 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1593
1693
  const targetValue = readAdhocTargetValue(directive);
1594
1694
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
1595
1695
  if (targetValue.kind === adhoc_enums_1.AdhocTargetKindEnum.WEBHOOK) {
1596
- const result = await executeAdhocWebhookDispatch(this.serviceTaskDispatcher, targetValue, {
1696
+ const webhookPayload = {
1597
1697
  ...payload,
1598
1698
  instanceId: instance.id,
1599
1699
  instanceTitle: instance.title,
1600
1700
  message,
1601
1701
  triggeredAt: new Date().toISOString(),
1602
- });
1603
- await activityRepository.save(activityRepository.create({
1604
- actorMemberId: null,
1605
- eventType: result.ok
1606
- ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
1607
- : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
1702
+ };
1703
+ await this.dispatchWebhookAfterCommit(manager, {
1608
1704
  instanceId: instance.id,
1609
- nodeId: directive.originNodeId,
1610
- payload: {
1611
- action: 'ADHOC_WEBHOOK',
1612
- directiveId: directive.id,
1613
- error: result.error,
1614
- ok: result.ok,
1615
- status: result.status,
1616
- url: targetValue.webhookUrl,
1617
- },
1618
- taskId: null,
1619
- }));
1705
+ readActivityLog: (result) => ({
1706
+ actorMemberId: null,
1707
+ eventType: result.ok
1708
+ ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
1709
+ : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
1710
+ instanceId: instance.id,
1711
+ nodeId: directive.originNodeId,
1712
+ payload: {
1713
+ action: 'ADHOC_WEBHOOK',
1714
+ directiveId: directive.id,
1715
+ error: result.error,
1716
+ ok: result.ok,
1717
+ status: result.status,
1718
+ url: targetValue.webhookUrl,
1719
+ },
1720
+ taskId: null,
1721
+ }),
1722
+ send: () => executeAdhocWebhookDispatch(this.serviceTaskDispatcher, targetValue, webhookPayload),
1723
+ });
1620
1724
  return;
1621
1725
  }
1622
1726
  try {
@@ -1667,29 +1771,38 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1667
1771
  })));
1668
1772
  }
1669
1773
  dryRunApprovalWorkflow(input) {
1774
+ // Checked first: parsing happens outside the time budget below.
1775
+ if ([
1776
+ input.workflowDefinitionJson,
1777
+ input.formDataJson,
1778
+ input.initiatorMetadataSnapshotJson ?? '',
1779
+ ].some((json) => json.length > MAX_DRY_RUN_INPUT_LENGTH)) {
1780
+ return createFailedDryRunResult('試跑資料過大,請縮小流程或測試資料後再試');
1781
+ }
1670
1782
  const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
1671
1783
  const formData = parseJsonObject(input.formDataJson, 'formDataJson');
1672
1784
  const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
1673
1785
  ? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
1674
1786
  : readDefaultInitiatorMetadataSnapshot(input.initiatorMemberId);
1675
1787
  try {
1788
+ // The CEL engine loads outside the budget so an interrupt can never land
1789
+ // halfway through loading a module.
1790
+ this.conditionService.prepareEngine((0, expression_dialect_1.readWorkflowExpressionDialect)(workflowDefinition));
1791
+ // The outer budget bounds the whole walk, and the operation budget
1792
+ // bounds the expressions inside it. Both are charged to this dry run only.
1676
1793
  return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
1677
1794
  errors: [],
1678
- steps: this.simulateWorkflow({
1795
+ steps: (0, expression_budget_1.runExpressionOperation)(() => (0, time_budget_1.runWithinTimeBudget)(() => this.simulateWorkflow({
1679
1796
  formData,
1680
1797
  initiatorMemberId: input.initiatorMemberId,
1681
1798
  initiatorMetadataSnapshot,
1682
1799
  workflowDefinition,
1683
- }),
1800
+ }), DRY_RUN_TIME_BUDGET_MS)),
1684
1801
  valid: true,
1685
1802
  });
1686
1803
  }
1687
1804
  catch (error) {
1688
- return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
1689
- errors: [error instanceof Error ? error.message : 'Dry run failed'],
1690
- steps: [],
1691
- valid: false,
1692
- });
1805
+ return createFailedDryRunResult(readDryRunFailureMessage(error));
1693
1806
  }
1694
1807
  }
1695
1808
  simulateWorkflow(input) {
@@ -1697,8 +1810,18 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1697
1810
  if (!startNode) {
1698
1811
  throw new common_1.BadRequestException('Workflow does not include a startEvent');
1699
1812
  }
1813
+ // Every branch re-walks whatever follows it, so a chain of gateways doubles
1814
+ // the work at each one while the depth limit never trips. Closure state
1815
+ // because all the branches of one dry run share this one count.
1816
+ let visits = 0;
1700
1817
  return this.simulateWorkflowPath({
1701
1818
  ...input,
1819
+ chargeVisit: () => {
1820
+ visits += 1;
1821
+ if (visits > MAX_DRY_RUN_VISITS) {
1822
+ throw new common_1.ConflictException(DRY_RUN_TOO_COMPLEX_MESSAGE);
1823
+ }
1824
+ },
1702
1825
  depth: 0,
1703
1826
  incomingEdge: null,
1704
1827
  lastDecision: null,
@@ -1711,6 +1834,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1711
1834
  if (input.depth > MAX_PROCESSING_STEPS) {
1712
1835
  throw new common_1.ConflictException('Dry run exceeded maximum processing steps');
1713
1836
  }
1837
+ input.chargeVisit();
1714
1838
  const node = readWorkflowNodeOrThrow(input.workflowDefinition, input.nodeId);
1715
1839
  const visitKey = `${input.nodeId}:${input.incomingEdge?.id ?? 'start'}`;
1716
1840
  const entryCondition = readNodeEntryCondition(node);
@@ -1732,7 +1856,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1732
1856
  ];
1733
1857
  }
1734
1858
  const nextVisitedNodeIds = new Set([...input.visitedNodeIds, visitKey]);
1735
- const entryConditionMet = this.conditionService.evaluateBoolean(entryCondition, buildDryRunExpressionContext(input, input.lastDecision), `workflow.nodes.${node.id}.data.entryCondition`);
1859
+ const entryConditionMet = this.conditionService.evaluateBoolean(entryCondition, buildDryRunExpressionContext(input, input.lastDecision), `workflow.nodes.${node.id}.data.entryCondition`, {
1860
+ allowedRootIdentifiers: DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
1861
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
1862
+ });
1736
1863
  const assignment = entryConditionMet && node.type === 'userTask'
1737
1864
  ? this.resolveDryRunAssignment(input, node)
1738
1865
  : null;
@@ -1824,11 +1951,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1824
1951
  };
1825
1952
  }
1826
1953
  evaluateDryRunEdge(input, edge, lastDecision) {
1827
- const expression = edge.data.condition?.trim();
1828
- if (expression && !isListValuedStructuredCondition(edge, input.formData)) {
1829
- return this.conditionService.evaluateBoolean(expression, buildDryRunExpressionContext(input, lastDecision), `workflow.edges.${edge.id}.data.condition`);
1954
+ const expression = readEvaluableEdgeExpression(edge, input.formData);
1955
+ if (!expression) {
1956
+ return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: input.formData });
1830
1957
  }
1831
- return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: input.formData });
1958
+ return evaluateWithStructuredFallback(edge, input.formData, (message) => this.logger.warn(`dry-run ${message}`), () => this.conditionService.evaluateBoolean(expression, buildDryRunExpressionContext(input, lastDecision), `workflow.edges.${edge.id}.data.condition`, {
1959
+ allowedRootIdentifiers: DRY_RUN_EDGE_ROOT_IDENTIFIERS,
1960
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
1961
+ }));
1832
1962
  }
1833
1963
  /**
1834
1964
  * The dry-run assignee of a user task. Only resolvers whose dry run yields
@@ -1926,7 +2056,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1926
2056
  return findMemberIdsInValue(readValueAtPath(input.formData, resolver.formPath));
1927
2057
  }
1928
2058
  if (resolver.type === 'EXPRESSION') {
1929
- return findMemberIdsInValue(this.conditionService.evaluateValue(resolver.expression, buildDryRunExpressionContext(input, null), `${label}.expression`));
2059
+ return findMemberIdsInValue(this.conditionService.evaluateValue(resolver.expression, buildDryRunExpressionContext(input, null), `${label}.expression`, {
2060
+ allowedRootIdentifiers: DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
2061
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
2062
+ }));
1930
2063
  }
1931
2064
  if (resolver.type === 'ORG_MANAGER') {
1932
2065
  const managerMemberIds = findMemberIdsInValue(readManagerMemberIdFromInitiatorSnapshot(input.initiatorMetadataSnapshot, resolver.levelsUp));
@@ -2138,7 +2271,18 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2138
2271
  if (!currentVersion?.formDefinitionVersionId) {
2139
2272
  return false;
2140
2273
  }
2141
- return this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel');
2274
+ // A policy in a dialect this version does not know cannot be evaluated,
2275
+ // so the template is not launchable (fail closed), but only that one:
2276
+ // publishing does not check the field yet (Phase 23 P2), and one such
2277
+ // version must not break the list for every member.
2278
+ if (hasExpressionText(currentVersion.initiatorPolicyCel) &&
2279
+ !isReadableExpressionDialect(currentVersion.workflowDefinition)) {
2280
+ this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of launchable templates: unknown expression dialect`);
2281
+ return false;
2282
+ }
2283
+ // Each template is its own operation, so one slow template cannot use
2284
+ // up the allowance of the rest of the list.
2285
+ return (0, expression_budget_1.runExpressionOperation)(() => this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel', readExpressionDialectOptions(currentVersion.initiatorPolicyCel, currentVersion.workflowDefinition)));
2142
2286
  });
2143
2287
  }
2144
2288
  async listWorkflowTokens(instanceId, scope) {
@@ -2592,7 +2736,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2592
2736
  }
2593
2737
  isNodeEntryConditionMet(instance, node) {
2594
2738
  const entryCondition = readNodeEntryCondition(node);
2595
- return this.conditionService.evaluateBoolean(entryCondition, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`);
2739
+ return this.conditionService.evaluateBoolean(entryCondition, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`, readExpressionDialectOptions(entryCondition, instance.workflowSnapshot));
2596
2740
  }
2597
2741
  async skipNodeByEntryCondition(manager, instance, token, node) {
2598
2742
  await this.skipNode(manager, instance, token, node, {
@@ -2659,15 +2803,17 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2659
2803
  throw new common_1.ConflictException(`Workflow node ${node.id} has no matching outgoing edge`);
2660
2804
  }
2661
2805
  async evaluateWorkflowEdge(manager, instance, node, edge) {
2662
- const expression = edge.data.condition?.trim();
2663
- if (expression &&
2664
- !isListValuedStructuredCondition(edge, instance.formData)) {
2665
- return this.conditionService.evaluateBoolean(expression, {
2666
- ...buildWorkflowExpressionContext(instance),
2667
- lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
2668
- }, `workflow.edges.${edge.id}.data.condition`);
2806
+ const expression = readEvaluableEdgeExpression(edge, instance.formData);
2807
+ if (!expression) {
2808
+ return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, {
2809
+ formData: instance.formData,
2810
+ });
2669
2811
  }
2670
- return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: instance.formData });
2812
+ const context = {
2813
+ ...buildWorkflowExpressionContext(instance),
2814
+ lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
2815
+ };
2816
+ return evaluateWithStructuredFallback(edge, instance.formData, (message) => this.logger.warn(`instance=${instance.id} ${message}`), () => this.conditionService.evaluateBoolean(expression, context, `workflow.edges.${edge.id}.data.condition`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) }));
2671
2817
  }
2672
2818
  async readLastDecisionContext(manager, instanceId, nodeId) {
2673
2819
  const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
@@ -3491,7 +3637,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3491
3637
  });
3492
3638
  }
3493
3639
  async executeTaskReassignment(preRead, options) {
3494
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
3640
+ return this.runEngineTransaction(async (manager) => {
3495
3641
  try {
3496
3642
  const plan = await this.planTaskReassignment(manager, preRead.id, options);
3497
3643
  if (options.dryRun) {
@@ -3868,7 +4014,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3868
4014
  async delegateResolvedCandidates(manager, instance, nodeId, resolvedCandidates, gradeGated) {
3869
4015
  // One batch for the node, so the host directory is asked once for every
3870
4016
  // candidate's delegation agents rather than once per candidate.
3871
- const resolutions = await this.delegationService.resolveAssignees(resolvedCandidates.map((candidate) => candidate.memberId), this.buildDelegationContext(instance, nodeId));
4017
+ const resolutions = await this.delegationService.resolveAssignees(resolvedCandidates.map((candidate) => candidate.memberId), { ...this.buildDelegationContext(instance, nodeId), manager });
3872
4018
  const resolved = resolvedCandidates.map((candidate, index) => ({
3873
4019
  candidate,
3874
4020
  resolution: resolutions[index],
@@ -3890,7 +4036,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3890
4036
  }
3891
4037
  /** One member's delegation, grade-gated as {@link applyGradeGate} says. */
3892
4038
  async resolveDelegation(manager, principalMemberId, context, gradeGated) {
3893
- const resolution = await this.delegationService.resolveAssignee(principalMemberId, context);
4039
+ const resolution = await this.delegationService.resolveAssignee(principalMemberId, { ...context, manager });
3894
4040
  const memberIds = gradeGated
3895
4041
  ? readGradeGateMemberIds(principalMemberId, resolution)
3896
4042
  : [];
@@ -3946,7 +4092,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3946
4092
  return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`);
3947
4093
  }
3948
4094
  if (resolver.type === 'EXPRESSION') {
3949
- return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`), label, subject, '依運算式');
4095
+ return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) }), label, subject, '依運算式');
3950
4096
  }
3951
4097
  if (resolver.type === 'POSITION') {
3952
4098
  return this.resolvePositionCandidates(manager, resolver.positionId, label, subject);
@@ -4468,34 +4614,39 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4468
4614
  const action = node.data.action;
4469
4615
  if (action.type === 'WEBHOOK') {
4470
4616
  const payload = typeof action.payload === 'string' && action.payload.trim()
4471
- ? this.conditionService.evaluateValue(action.payload, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`)
4617
+ ? this.conditionService.evaluateValue(action.payload, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`, {
4618
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot),
4619
+ })
4472
4620
  : {
4473
4621
  instance: buildWorkflowExpressionContext(instance).instance,
4474
4622
  triggeredAt: new Date().toISOString(),
4475
4623
  };
4476
- const result = await executeWebhookServiceAction(this.serviceTaskDispatcher, action, payload);
4477
- await activityRepository.save(activityRepository.create({
4478
- actorMemberId: null,
4479
- eventType: result.ok
4480
- ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
4481
- : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
4624
+ await this.dispatchWebhookAfterCommit(manager, {
4482
4625
  instanceId: instance.id,
4483
- nodeId: node.id,
4484
- payload: {
4485
- action: 'WEBHOOK',
4486
- error: result.error,
4487
- ok: result.ok,
4488
- status: result.status,
4489
- tokenId: token.id,
4490
- url: action.url,
4491
- },
4492
- taskId: null,
4493
- }));
4626
+ readActivityLog: (result) => ({
4627
+ actorMemberId: null,
4628
+ eventType: result.ok
4629
+ ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
4630
+ : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
4631
+ instanceId: instance.id,
4632
+ nodeId: node.id,
4633
+ payload: {
4634
+ action: 'WEBHOOK',
4635
+ error: result.error,
4636
+ ok: result.ok,
4637
+ status: result.status,
4638
+ tokenId: token.id,
4639
+ url: action.url,
4640
+ },
4641
+ taskId: null,
4642
+ }),
4643
+ send: () => executeWebhookServiceAction(this.serviceTaskDispatcher, action, payload),
4644
+ });
4494
4645
  await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
4495
4646
  return;
4496
4647
  }
4497
4648
  if (action.type === 'SET_FORM_FIELD') {
4498
- const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`);
4649
+ const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) });
4499
4650
  assertServiceTaskTextValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
4500
4651
  const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
4501
4652
  // A service task runs inside this transaction, so it cannot call a host
@@ -5297,22 +5448,67 @@ function readNodeEntryCondition(node) {
5297
5448
  return null;
5298
5449
  }
5299
5450
  /**
5300
- * A designer-built condition on a multiple-selection field (checkbox, or a
5301
- * select / autocomplete in `multiple` mode), whose submitted value is a list.
5302
- * Definitions saved before the designer compiled such fields with list
5303
- * semantics carry a single-value CEL (`form.tags == "A"`), which is false for
5304
- * `["A"]` — `!=` is always true and `!= ""` counts `[]` as filled — so those
5305
- * edges never routed as designed. The structured fields the designer stores
5306
- * beside the CEL are evaluated instead: `evaluateWorkflowEdgeCondition`
5307
- * already reads a list as contains / does not contain and an empty list as
5308
- * empty, the meaning the current designer compiles too. A hand-written CEL
5309
- * carries no structured fields and is never overridden.
5451
+ * The stored CEL an edge is evaluated by, or `null` when its structured fields
5452
+ * answer directly: no CEL, or a designer-built edge whose value is a list
5453
+ * (`evaluateWorkflowEdgeCondition` reads a list as contains / does not contain
5454
+ * and an empty list as empty, which the stored CEL of older definitions did
5455
+ * not).
5310
5456
  */
5311
- function isListValuedStructuredCondition(edge, formData) {
5457
+ function readEvaluableEdgeExpression(edge, formData) {
5458
+ const expression = edge.data.condition?.trim();
5312
5459
  const fieldKey = edge.data.conditionFieldKey;
5313
- return Boolean(fieldKey &&
5460
+ const listValued = Boolean(fieldKey &&
5314
5461
  edge.data.conditionOperator &&
5315
- Array.isArray(formData[fieldKey]));
5462
+ Array.isArray((0, workflow_form_value_1.readOwnFormValue)(formData, fieldKey)));
5463
+ return expression && !listValued ? expression : null;
5464
+ }
5465
+ /**
5466
+ * Evaluates an edge's stored CEL and, only when it fails because the edge's
5467
+ * own field is unfilled, answers from the structured fields instead. The one
5468
+ * place both the actual run and the designer dry run decide that, so the two
5469
+ * cannot drift apart.
5470
+ *
5471
+ * Published definitions and in-flight instances keep the CEL they were
5472
+ * stored with, and that CEL throws for an optional field left untouched: the
5473
+ * key is absent (`Identifier "agent" not found`), or an ordered comparison
5474
+ * meets `null` or `""` (`null > 5`, `"" > 5`). Only then does the structured
5475
+ * evaluator answer, reading the value as unfilled (`EQUALS` and the ordered
5476
+ * comparisons false, `NOT_EQUALS` and `IS_EMPTY` true, `IS_FILLED` false).
5477
+ *
5478
+ * - `EXPRESSION_MISSING_KEY` falls back only when the missing key *is* the
5479
+ * edge's field. A CEL edited by hand to also read another key
5480
+ * (`form.agent != null && initiator.department == "X"`) fails as written
5481
+ * when that other key is missing, even if the edge's field is unfilled.
5482
+ * - `EXPRESSION_TYPE_ERROR` carries no key, so it falls back whenever the
5483
+ * edge's field is unfilled, and each such fallback is logged (`warn`, no
5484
+ * values) so an unrelated type error answered this way stays traceable.
5485
+ * - Everything else is rethrown: a stored CEL that evaluates keeps its
5486
+ * result, so no route that worked before changes; an error while the
5487
+ * edge's field is filled points at the CEL itself; a hand-written CEL has
5488
+ * no structured fields; a budget overrun or a function failure is never
5489
+ * absorbed.
5490
+ */
5491
+ function evaluateWithStructuredFallback(edge, formData, warn, evaluate) {
5492
+ try {
5493
+ return evaluate();
5494
+ }
5495
+ catch (error) {
5496
+ const fieldKey = edge.data.conditionFieldKey;
5497
+ if (!fieldKey ||
5498
+ !edge.data.conditionOperator ||
5499
+ !(error instanceof expression_errors_1.ExpressionRuntimeError) ||
5500
+ !(0, workflow_form_value_1.isEmptyWorkflowValue)((0, workflow_form_value_1.readOwnFormValue)(formData, fieldKey))) {
5501
+ throw error;
5502
+ }
5503
+ if (error.code === 'EXPRESSION_MISSING_KEY' && error.keyName === fieldKey) {
5504
+ return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData });
5505
+ }
5506
+ if (error.code === 'EXPRESSION_TYPE_ERROR') {
5507
+ warn(`code=EDGE_STRUCTURED_FALLBACK edge=${edge.id} field=${fieldKey} reason=EXPRESSION_TYPE_ERROR`);
5508
+ return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData });
5509
+ }
5510
+ throw error;
5511
+ }
5316
5512
  }
5317
5513
  function edgeHasCondition(edge) {
5318
5514
  return Boolean(edge.data.condition?.trim() ||
@@ -5617,6 +5813,33 @@ function normalizeFormFieldPath(path) {
5617
5813
  }
5618
5814
  return segments;
5619
5815
  }
5816
+ /**
5817
+ * The message a failed dry run shows its caller. Only the fixed budget text, an
5818
+ * HTTP exception's own message, or an expression error's label and code reach
5819
+ * this point. Any other error is replaced with a generic message, since its text
5820
+ * may carry engine or context details.
5821
+ */
5822
+ function readDryRunFailureMessage(error) {
5823
+ if (error instanceof time_budget_1.TimeBudgetExceededError) {
5824
+ return DRY_RUN_TOO_COMPLEX_MESSAGE;
5825
+ }
5826
+ if (error instanceof expression_errors_1.ExpressionRuntimeError) {
5827
+ return error.keyName
5828
+ ? `${error.message}(找不到「${error.keyName}」)`
5829
+ : error.message;
5830
+ }
5831
+ if (error instanceof common_1.HttpException) {
5832
+ return error.message;
5833
+ }
5834
+ return 'Dry run failed';
5835
+ }
5836
+ function createFailedDryRunResult(message) {
5837
+ return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
5838
+ errors: [message],
5839
+ steps: [],
5840
+ valid: false,
5841
+ });
5842
+ }
5620
5843
  async function executeWebhookServiceAction(serviceTaskDispatcher, action, payload) {
5621
5844
  try {
5622
5845
  return await serviceTaskDispatcher.dispatchWebhook({