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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +36 -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 +105 -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-engine.service.d.ts +24 -0
  129. package/src/lib/workflow-engine/workflow-engine.service.js +240 -65
  130. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  131. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +4 -0
  132. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -1
  133. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +3 -1
  134. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +11 -2
  135. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -1
  136. package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +8 -1
  137. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +70 -27
  138. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -1
  139. package/src/lib/workflow-webhook/workflow-webhook-expression.d.ts +57 -0
  140. package/src/lib/workflow-webhook/workflow-webhook-expression.js +123 -0
  141. package/src/lib/workflow-webhook/workflow-webhook-expression.js.map +1 -0
  142. package/src/lib/workflow-webhook/workflow-webhook.module.js +4 -0
  143. package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
  144. package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +7 -0
  145. package/src/lib/workflow-webhook/workflow-webhook.validator.js +29 -6
  146. package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -1
@@ -48,9 +48,17 @@ 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
50
  const workflow_dry_run_object_1 = require("./workflow-dry-run.object");
51
+ const time_budget_1 = require("./time-budget");
52
+ const expression_budget_1 = require("../condition/expression-budget");
53
+ const expression_errors_1 = require("../condition/expression-errors");
54
+ const expression_dialect_1 = require("../condition/expression-dialect");
51
55
  const workflow_service_task_dispatcher_token_1 = require("./workflow-service-task-dispatcher.token");
52
56
  const workflow_token_entity_1 = require("./workflow-token.entity");
53
57
  const bpm_root_options_1 = require("../bpm/bpm-root-options");
58
+ const MAX_DRY_RUN_INPUT_LENGTH = 1_048_576;
59
+ const MAX_DRY_RUN_VISITS = 10_000;
60
+ const DRY_RUN_TIME_BUDGET_MS = 2_000;
61
+ const DRY_RUN_TOO_COMPLEX_MESSAGE = '流程或條件的運算量過大,無法完成試跑,請減少連續的閘道分支或簡化條件';
54
62
  const MAX_PROCESSING_STEPS = 500;
55
63
  exports.MAX_APPROVAL_INSTANCE_BUSINESS_KEY_LENGTH = 200;
56
64
  const APPROVAL_INSTANCE_BUSINESS_KEY_INDEX = 'approval_instances_business_key_key';
@@ -58,6 +66,40 @@ const APPROVAL_INSTANCE_BUSINESS_KEY_INDEX = 'approval_instances_business_key_ke
58
66
  const ORG_MANAGER_CHAIN_MAX_HOPS = 20;
59
67
  const DEFAULT_APPROVAL_HISTORY_TASK_LIMIT = 50;
60
68
  const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
69
+ // The dry run checks each expression against the roots its use point may read,
70
+ // as publish does. Keep these in step with `WORKFLOW_EXPRESSION_ROOT_IDENTIFIERS`
71
+ // in template.service.ts; P2 moves both to one place.
72
+ const DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS = [
73
+ 'env',
74
+ 'form',
75
+ 'formData',
76
+ 'initiator',
77
+ 'instance',
78
+ ];
79
+ const DRY_RUN_EDGE_ROOT_IDENTIFIERS = [
80
+ ...DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
81
+ 'lastDecision',
82
+ ];
83
+ const hasExpressionText = (expression) => typeof expression === 'string' && expression.trim() !== '';
84
+ /**
85
+ * Whether a definition's dialect can be read: missing (`cel-legacy`) or one
86
+ * this version knows. Never throws, unlike `readWorkflowExpressionDialect`.
87
+ */
88
+ const isReadableExpressionDialect = (definition) => {
89
+ const dialect = definition?.expressionDialect;
90
+ return (dialect === undefined || dialect === null || (0, expression_dialect_1.isExpressionDialect)(dialect));
91
+ };
92
+ /**
93
+ * Options for evaluating an expression that may be blank. The dialect is read
94
+ * only when there is an expression: an unknown dialect is refused because the
95
+ * expression cannot be evaluated as its author tested it, and a blank one is
96
+ * never evaluated at all. Publishing does not check the field yet (Phase 23
97
+ * P2), so reading it eagerly would turn a stray field on a version with no
98
+ * expressions into an error, where before dialects existed it was ignored.
99
+ */
100
+ const readExpressionDialectOptions = (expression, definition) => hasExpressionText(expression)
101
+ ? { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(definition) }
102
+ : {};
61
103
  /**
62
104
  * Delegation-chain reason recorded for an approver-initiated transfer, as
63
105
  * opposed to {@link SLA_ESCALATION_DELEGATION_REASON}.
@@ -152,6 +194,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
152
194
  this.formDataSourceValueResolver = formDataSourceValueResolver;
153
195
  this.webhookDeliveryService = webhookDeliveryService;
154
196
  this.logger = new common_1.Logger(WorkflowEngineService_1.name);
197
+ /** Webhooks each open engine transaction will send once it has committed. */
198
+ this.deferredWebhookDispatches = new WeakMap();
155
199
  // A binding under the token wins: that is either the host's own provider
156
200
  // or `workflowServiceTaskDispatcherProvider`, both of them deliberate. The
157
201
  // runtime instance from `BPMRootModule`'s async factory comes next.
@@ -177,6 +221,59 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
177
221
  readOrganizationToday() {
178
222
  return (0, organization_time_zone_1.formatDateOnlyInTimeZone)(new Date(), this.organizationTimeZone);
179
223
  }
224
+ /**
225
+ * Opens an engine transaction whose webhooks wait for its commit. A webhook
226
+ * sent from inside would hold the case lock and a pooled connection for as
227
+ * long as the far end takes to answer, so a slow endpoint could stall the
228
+ * case and then, a connection at a time, the whole API. A transaction that
229
+ * rolls back sends nothing.
230
+ *
231
+ * The caller still waits for the sends, so the outcome is in the activity log
232
+ * by the time a mutation returns, as it always has been.
233
+ */
234
+ async runEngineTransaction(work) {
235
+ // Mutable on purpose: every step of the transaction appends to it, and
236
+ // threading it through each of them would change a dozen signatures.
237
+ const dispatches = [];
238
+ // One transaction is one operation: its evaluations share one budget.
239
+ const result = await (0, expression_budget_1.runExpressionOperation)(() => this.approvalInstanceRepository.manager.transaction((manager) => {
240
+ this.deferredWebhookDispatches.set(manager, dispatches);
241
+ return work(manager);
242
+ }));
243
+ await dispatches.reduce(async (previous, dispatch) => {
244
+ await previous;
245
+ await this.sendCommittedWebhook(dispatch);
246
+ }, Promise.resolve());
247
+ return result;
248
+ }
249
+ /**
250
+ * Queues a webhook for after the commit. A manager this service did not open
251
+ * has no queue and no commit it can see, so that webhook goes out on the spot
252
+ * rather than being dropped.
253
+ */
254
+ async dispatchWebhookAfterCommit(manager, dispatch) {
255
+ const queue = this.deferredWebhookDispatches.get(manager);
256
+ if (queue) {
257
+ queue.push(dispatch);
258
+ return;
259
+ }
260
+ const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
261
+ const result = await dispatch.send();
262
+ await activityRepository.save(activityRepository.create(dispatch.readActivityLog(result)));
263
+ }
264
+ /**
265
+ * The transaction has committed and the mutation has succeeded, so nothing
266
+ * here may fail it: a result that cannot be recorded is only logged.
267
+ */
268
+ async sendCommittedWebhook(dispatch) {
269
+ try {
270
+ const result = await dispatch.send();
271
+ await this.activityLogRepository.save(this.activityLogRepository.create(dispatch.readActivityLog(result)));
272
+ }
273
+ catch (error) {
274
+ this.logger.error(`Could not record a webhook result for instance ${dispatch.instanceId}: ${error instanceof Error ? error.message : String(error)}`);
275
+ }
276
+ }
180
277
  async submitApprovalInstance(input, authContext) {
181
278
  if (input.submittedByMemberId !== undefined &&
182
279
  input.submittedByMemberId !== null &&
@@ -219,7 +316,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
219
316
  const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
220
317
  ? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
221
318
  : await this.readDefaultInitiatorMetadataSnapshot(input.initiatorMemberId);
222
- if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel')) {
319
+ if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel',
320
+ // The policy is written in its version's dialect (ADR 22 §3.3).
321
+ readExpressionDialectOptions(templateVersion.initiatorPolicyCel, templateVersion.workflowDefinition))) {
223
322
  throw new common_1.ConflictException('Current member is not allowed to start this approval template');
224
323
  }
225
324
  // Named here, before the transaction, so the completion notice that later
@@ -233,7 +332,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
233
332
  if (!startNode) {
234
333
  throw new common_1.BadRequestException('Published workflow does not include a startEvent');
235
334
  }
236
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
335
+ return this.runEngineTransaction(async (manager) => {
237
336
  const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
238
337
  const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
239
338
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
@@ -370,7 +469,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
370
469
  return { formDataOptionSnapshot, instance };
371
470
  }
372
471
  async processInstance(instanceId) {
373
- await this.approvalInstanceRepository.manager.transaction(async (manager) => {
472
+ await this.runEngineTransaction(async (manager) => {
374
473
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
375
474
  instanceId,
376
475
  ]);
@@ -397,7 +496,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
397
496
  });
398
497
  }
399
498
  async decideTask(input, options = {}) {
400
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
499
+ return this.runEngineTransaction(async (manager) => {
401
500
  const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
402
501
  const taskCandidateRepository = manager.getRepository(task_candidate_entity_1.TaskCandidateEntity);
403
502
  const taskDecisionRepository = manager.getRepository(task_decision_entity_1.TaskDecisionEntity);
@@ -760,7 +859,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
760
859
  });
761
860
  }
762
861
  async cancelApprovalInstance(input) {
763
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
862
+ return this.runEngineTransaction(async (manager) => {
764
863
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
765
864
  input.instanceId,
766
865
  ]);
@@ -801,7 +900,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
801
900
  * returned unchanged.
802
901
  */
803
902
  async expireApprovalInstance(input) {
804
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
903
+ return this.runEngineTransaction(async (manager) => {
805
904
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
806
905
  input.instanceId,
807
906
  ]);
@@ -858,7 +957,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
858
957
  const preparation = this.formDataSourceValueResolver
859
958
  ? await this.prepareResubmitFormDataOptions(input, submittedFormData, authContext ?? createWorkflowAuthContext(input.initiatorMemberId))
860
959
  : null;
861
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
960
+ return this.runEngineTransaction(async (manager) => {
862
961
  await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
863
962
  input.instanceId,
864
963
  ]);
@@ -947,7 +1046,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
947
1046
  });
948
1047
  }
949
1048
  async requestAdhocCountersign({ comment, requestedByMemberId, target, taskId, }) {
950
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1049
+ return this.runEngineTransaction(async (manager) => {
951
1050
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
952
1051
  const targetValue = buildAdhocTargetValue(target);
953
1052
  // Rejects WEBHOOK targets — countersigners must be members.
@@ -979,7 +1078,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
979
1078
  });
980
1079
  }
981
1080
  async requestAdhocPreApproval({ comment, onReject, requestedByMemberId, target, taskId, }) {
982
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1081
+ return this.runEngineTransaction(async (manager) => {
983
1082
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: true });
984
1083
  const targetValue = buildAdhocTargetValue(target);
985
1084
  // Rejects WEBHOOK targets — pre-approvers must be members.
@@ -1006,7 +1105,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1006
1105
  });
1007
1106
  }
1008
1107
  async configureAdhocNotification({ channels, requestedByMemberId, target, taskId, type, }) {
1009
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1108
+ return this.runEngineTransaction(async (manager) => {
1010
1109
  const { instance, node, task } = await this.loadAdhocOperationContext(manager, taskId, requestedByMemberId, { requireAllowAddSigner: false });
1011
1110
  const targetValue = buildAdhocTargetValue(target);
1012
1111
  const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
@@ -1029,7 +1128,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1029
1128
  });
1030
1129
  }
1031
1130
  async cancelAdhocDirective({ cancelledByMemberId, directiveId, }) {
1032
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
1131
+ return this.runEngineTransaction(async (manager) => {
1033
1132
  const directiveRepository = manager.getRepository(adhoc_directive_entity_1.AdhocDirectiveEntity);
1034
1133
  const directive = await directiveRepository.findOne({
1035
1134
  where: { id: directiveId },
@@ -1593,30 +1692,34 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1593
1692
  const targetValue = readAdhocTargetValue(directive);
1594
1693
  const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
1595
1694
  if (targetValue.kind === adhoc_enums_1.AdhocTargetKindEnum.WEBHOOK) {
1596
- const result = await executeAdhocWebhookDispatch(this.serviceTaskDispatcher, targetValue, {
1695
+ const webhookPayload = {
1597
1696
  ...payload,
1598
1697
  instanceId: instance.id,
1599
1698
  instanceTitle: instance.title,
1600
1699
  message,
1601
1700
  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,
1701
+ };
1702
+ await this.dispatchWebhookAfterCommit(manager, {
1608
1703
  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
- }));
1704
+ readActivityLog: (result) => ({
1705
+ actorMemberId: null,
1706
+ eventType: result.ok
1707
+ ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
1708
+ : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
1709
+ instanceId: instance.id,
1710
+ nodeId: directive.originNodeId,
1711
+ payload: {
1712
+ action: 'ADHOC_WEBHOOK',
1713
+ directiveId: directive.id,
1714
+ error: result.error,
1715
+ ok: result.ok,
1716
+ status: result.status,
1717
+ url: targetValue.webhookUrl,
1718
+ },
1719
+ taskId: null,
1720
+ }),
1721
+ send: () => executeAdhocWebhookDispatch(this.serviceTaskDispatcher, targetValue, webhookPayload),
1722
+ });
1620
1723
  return;
1621
1724
  }
1622
1725
  try {
@@ -1667,29 +1770,38 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1667
1770
  })));
1668
1771
  }
1669
1772
  dryRunApprovalWorkflow(input) {
1773
+ // Checked first: parsing happens outside the time budget below.
1774
+ if ([
1775
+ input.workflowDefinitionJson,
1776
+ input.formDataJson,
1777
+ input.initiatorMetadataSnapshotJson ?? '',
1778
+ ].some((json) => json.length > MAX_DRY_RUN_INPUT_LENGTH)) {
1779
+ return createFailedDryRunResult('試跑資料過大,請縮小流程或測試資料後再試');
1780
+ }
1670
1781
  const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
1671
1782
  const formData = parseJsonObject(input.formDataJson, 'formDataJson');
1672
1783
  const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
1673
1784
  ? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
1674
1785
  : readDefaultInitiatorMetadataSnapshot(input.initiatorMemberId);
1675
1786
  try {
1787
+ // The CEL engine loads outside the budget so an interrupt can never land
1788
+ // halfway through loading a module.
1789
+ this.conditionService.prepareEngine((0, expression_dialect_1.readWorkflowExpressionDialect)(workflowDefinition));
1790
+ // The outer budget bounds the whole walk, and the operation budget
1791
+ // bounds the expressions inside it. Both are charged to this dry run only.
1676
1792
  return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
1677
1793
  errors: [],
1678
- steps: this.simulateWorkflow({
1794
+ steps: (0, expression_budget_1.runExpressionOperation)(() => (0, time_budget_1.runWithinTimeBudget)(() => this.simulateWorkflow({
1679
1795
  formData,
1680
1796
  initiatorMemberId: input.initiatorMemberId,
1681
1797
  initiatorMetadataSnapshot,
1682
1798
  workflowDefinition,
1683
- }),
1799
+ }), DRY_RUN_TIME_BUDGET_MS)),
1684
1800
  valid: true,
1685
1801
  });
1686
1802
  }
1687
1803
  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
- });
1804
+ return createFailedDryRunResult(readDryRunFailureMessage(error));
1693
1805
  }
1694
1806
  }
1695
1807
  simulateWorkflow(input) {
@@ -1697,8 +1809,18 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1697
1809
  if (!startNode) {
1698
1810
  throw new common_1.BadRequestException('Workflow does not include a startEvent');
1699
1811
  }
1812
+ // Every branch re-walks whatever follows it, so a chain of gateways doubles
1813
+ // the work at each one while the depth limit never trips. Closure state
1814
+ // because all the branches of one dry run share this one count.
1815
+ let visits = 0;
1700
1816
  return this.simulateWorkflowPath({
1701
1817
  ...input,
1818
+ chargeVisit: () => {
1819
+ visits += 1;
1820
+ if (visits > MAX_DRY_RUN_VISITS) {
1821
+ throw new common_1.ConflictException(DRY_RUN_TOO_COMPLEX_MESSAGE);
1822
+ }
1823
+ },
1702
1824
  depth: 0,
1703
1825
  incomingEdge: null,
1704
1826
  lastDecision: null,
@@ -1711,6 +1833,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1711
1833
  if (input.depth > MAX_PROCESSING_STEPS) {
1712
1834
  throw new common_1.ConflictException('Dry run exceeded maximum processing steps');
1713
1835
  }
1836
+ input.chargeVisit();
1714
1837
  const node = readWorkflowNodeOrThrow(input.workflowDefinition, input.nodeId);
1715
1838
  const visitKey = `${input.nodeId}:${input.incomingEdge?.id ?? 'start'}`;
1716
1839
  const entryCondition = readNodeEntryCondition(node);
@@ -1732,7 +1855,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1732
1855
  ];
1733
1856
  }
1734
1857
  const nextVisitedNodeIds = new Set([...input.visitedNodeIds, visitKey]);
1735
- const entryConditionMet = this.conditionService.evaluateBoolean(entryCondition, buildDryRunExpressionContext(input, input.lastDecision), `workflow.nodes.${node.id}.data.entryCondition`);
1858
+ const entryConditionMet = this.conditionService.evaluateBoolean(entryCondition, buildDryRunExpressionContext(input, input.lastDecision), `workflow.nodes.${node.id}.data.entryCondition`, {
1859
+ allowedRootIdentifiers: DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
1860
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
1861
+ });
1736
1862
  const assignment = entryConditionMet && node.type === 'userTask'
1737
1863
  ? this.resolveDryRunAssignment(input, node)
1738
1864
  : null;
@@ -1826,7 +1952,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1826
1952
  evaluateDryRunEdge(input, edge, lastDecision) {
1827
1953
  const expression = edge.data.condition?.trim();
1828
1954
  if (expression && !isListValuedStructuredCondition(edge, input.formData)) {
1829
- return this.conditionService.evaluateBoolean(expression, buildDryRunExpressionContext(input, lastDecision), `workflow.edges.${edge.id}.data.condition`);
1955
+ return this.conditionService.evaluateBoolean(expression, buildDryRunExpressionContext(input, lastDecision), `workflow.edges.${edge.id}.data.condition`, {
1956
+ allowedRootIdentifiers: DRY_RUN_EDGE_ROOT_IDENTIFIERS,
1957
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
1958
+ });
1830
1959
  }
1831
1960
  return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: input.formData });
1832
1961
  }
@@ -1926,7 +2055,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1926
2055
  return findMemberIdsInValue(readValueAtPath(input.formData, resolver.formPath));
1927
2056
  }
1928
2057
  if (resolver.type === 'EXPRESSION') {
1929
- return findMemberIdsInValue(this.conditionService.evaluateValue(resolver.expression, buildDryRunExpressionContext(input, null), `${label}.expression`));
2058
+ return findMemberIdsInValue(this.conditionService.evaluateValue(resolver.expression, buildDryRunExpressionContext(input, null), `${label}.expression`, {
2059
+ allowedRootIdentifiers: DRY_RUN_WORKFLOW_ROOT_IDENTIFIERS,
2060
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
2061
+ }));
1930
2062
  }
1931
2063
  if (resolver.type === 'ORG_MANAGER') {
1932
2064
  const managerMemberIds = findMemberIdsInValue(readManagerMemberIdFromInitiatorSnapshot(input.initiatorMetadataSnapshot, resolver.levelsUp));
@@ -2138,7 +2270,18 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2138
2270
  if (!currentVersion?.formDefinitionVersionId) {
2139
2271
  return false;
2140
2272
  }
2141
- return this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel');
2273
+ // A policy in a dialect this version does not know cannot be evaluated,
2274
+ // so the template is not launchable (fail closed), but only that one:
2275
+ // publishing does not check the field yet (Phase 23 P2), and one such
2276
+ // version must not break the list for every member.
2277
+ if (hasExpressionText(currentVersion.initiatorPolicyCel) &&
2278
+ !isReadableExpressionDialect(currentVersion.workflowDefinition)) {
2279
+ this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of launchable templates: unknown expression dialect`);
2280
+ return false;
2281
+ }
2282
+ // Each template is its own operation, so one slow template cannot use
2283
+ // up the allowance of the rest of the list.
2284
+ return (0, expression_budget_1.runExpressionOperation)(() => this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel', readExpressionDialectOptions(currentVersion.initiatorPolicyCel, currentVersion.workflowDefinition)));
2142
2285
  });
2143
2286
  }
2144
2287
  async listWorkflowTokens(instanceId, scope) {
@@ -2592,7 +2735,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2592
2735
  }
2593
2736
  isNodeEntryConditionMet(instance, node) {
2594
2737
  const entryCondition = readNodeEntryCondition(node);
2595
- return this.conditionService.evaluateBoolean(entryCondition, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`);
2738
+ return this.conditionService.evaluateBoolean(entryCondition, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`, readExpressionDialectOptions(entryCondition, instance.workflowSnapshot));
2596
2739
  }
2597
2740
  async skipNodeByEntryCondition(manager, instance, token, node) {
2598
2741
  await this.skipNode(manager, instance, token, node, {
@@ -2665,7 +2808,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
2665
2808
  return this.conditionService.evaluateBoolean(expression, {
2666
2809
  ...buildWorkflowExpressionContext(instance),
2667
2810
  lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
2668
- }, `workflow.edges.${edge.id}.data.condition`);
2811
+ }, `workflow.edges.${edge.id}.data.condition`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) });
2669
2812
  }
2670
2813
  return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: instance.formData });
2671
2814
  }
@@ -3491,7 +3634,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3491
3634
  });
3492
3635
  }
3493
3636
  async executeTaskReassignment(preRead, options) {
3494
- return this.approvalInstanceRepository.manager.transaction(async (manager) => {
3637
+ return this.runEngineTransaction(async (manager) => {
3495
3638
  try {
3496
3639
  const plan = await this.planTaskReassignment(manager, preRead.id, options);
3497
3640
  if (options.dryRun) {
@@ -3868,7 +4011,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3868
4011
  async delegateResolvedCandidates(manager, instance, nodeId, resolvedCandidates, gradeGated) {
3869
4012
  // One batch for the node, so the host directory is asked once for every
3870
4013
  // 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));
4014
+ const resolutions = await this.delegationService.resolveAssignees(resolvedCandidates.map((candidate) => candidate.memberId), { ...this.buildDelegationContext(instance, nodeId), manager });
3872
4015
  const resolved = resolvedCandidates.map((candidate, index) => ({
3873
4016
  candidate,
3874
4017
  resolution: resolutions[index],
@@ -3890,7 +4033,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3890
4033
  }
3891
4034
  /** One member's delegation, grade-gated as {@link applyGradeGate} says. */
3892
4035
  async resolveDelegation(manager, principalMemberId, context, gradeGated) {
3893
- const resolution = await this.delegationService.resolveAssignee(principalMemberId, context);
4036
+ const resolution = await this.delegationService.resolveAssignee(principalMemberId, { ...context, manager });
3894
4037
  const memberIds = gradeGated
3895
4038
  ? readGradeGateMemberIds(principalMemberId, resolution)
3896
4039
  : [];
@@ -3946,7 +4089,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3946
4089
  return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`);
3947
4090
  }
3948
4091
  if (resolver.type === 'EXPRESSION') {
3949
- return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`), label, subject, '依運算式');
4092
+ return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) }), label, subject, '依運算式');
3950
4093
  }
3951
4094
  if (resolver.type === 'POSITION') {
3952
4095
  return this.resolvePositionCandidates(manager, resolver.positionId, label, subject);
@@ -4468,34 +4611,39 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4468
4611
  const action = node.data.action;
4469
4612
  if (action.type === 'WEBHOOK') {
4470
4613
  const payload = typeof action.payload === 'string' && action.payload.trim()
4471
- ? this.conditionService.evaluateValue(action.payload, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`)
4614
+ ? this.conditionService.evaluateValue(action.payload, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`, {
4615
+ dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot),
4616
+ })
4472
4617
  : {
4473
4618
  instance: buildWorkflowExpressionContext(instance).instance,
4474
4619
  triggeredAt: new Date().toISOString(),
4475
4620
  };
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,
4621
+ await this.dispatchWebhookAfterCommit(manager, {
4482
4622
  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
- }));
4623
+ readActivityLog: (result) => ({
4624
+ actorMemberId: null,
4625
+ eventType: result.ok
4626
+ ? workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
4627
+ : workflow_engine_enums_1.ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
4628
+ instanceId: instance.id,
4629
+ nodeId: node.id,
4630
+ payload: {
4631
+ action: 'WEBHOOK',
4632
+ error: result.error,
4633
+ ok: result.ok,
4634
+ status: result.status,
4635
+ tokenId: token.id,
4636
+ url: action.url,
4637
+ },
4638
+ taskId: null,
4639
+ }),
4640
+ send: () => executeWebhookServiceAction(this.serviceTaskDispatcher, action, payload),
4641
+ });
4494
4642
  await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
4495
4643
  return;
4496
4644
  }
4497
4645
  if (action.type === 'SET_FORM_FIELD') {
4498
- const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`);
4646
+ 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
4647
  assertServiceTaskTextValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
4500
4648
  const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
4501
4649
  // A service task runs inside this transaction, so it cannot call a host
@@ -5617,6 +5765,33 @@ function normalizeFormFieldPath(path) {
5617
5765
  }
5618
5766
  return segments;
5619
5767
  }
5768
+ /**
5769
+ * The message a failed dry run shows its caller. Only the fixed budget text, an
5770
+ * HTTP exception's own message, or an expression error's label and code reach
5771
+ * this point. Any other error is replaced with a generic message, since its text
5772
+ * may carry engine or context details.
5773
+ */
5774
+ function readDryRunFailureMessage(error) {
5775
+ if (error instanceof time_budget_1.TimeBudgetExceededError) {
5776
+ return DRY_RUN_TOO_COMPLEX_MESSAGE;
5777
+ }
5778
+ if (error instanceof expression_errors_1.ExpressionRuntimeError) {
5779
+ return error.keyName
5780
+ ? `${error.message}(找不到「${error.keyName}」)`
5781
+ : error.message;
5782
+ }
5783
+ if (error instanceof common_1.HttpException) {
5784
+ return error.message;
5785
+ }
5786
+ return 'Dry run failed';
5787
+ }
5788
+ function createFailedDryRunResult(message) {
5789
+ return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
5790
+ errors: [message],
5791
+ steps: [],
5792
+ valid: false,
5793
+ });
5794
+ }
5620
5795
  async function executeWebhookServiceAction(serviceTaskDispatcher, action, payload) {
5621
5796
  try {
5622
5797
  return await serviceTaskDispatcher.dispatchWebhook({