@rytass/bpm-core-nestjs-module 0.13.2 → 0.13.4

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 (145) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +13 -6
  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/attachment/attachment-storage.provider.d.ts +11 -0
  7. package/src/lib/attachment/attachment-storage.provider.js +15 -1
  8. package/src/lib/attachment/attachment-storage.provider.js.map +1 -1
  9. package/src/lib/attachment/attachment.module.js +16 -1
  10. package/src/lib/attachment/attachment.module.js.map +1 -1
  11. package/src/lib/bpm/bpm-root-options.d.ts +134 -0
  12. package/src/lib/bpm/bpm-root-options.js +61 -0
  13. package/src/lib/bpm/bpm-root-options.js.map +1 -0
  14. package/src/lib/bpm/bpm-root.module.d.ts +101 -137
  15. package/src/lib/bpm/bpm-root.module.js +83 -118
  16. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  17. package/src/lib/bpm/index.d.ts +1 -0
  18. package/src/lib/bpm/index.js +1 -0
  19. package/src/lib/bpm/index.js.map +1 -1
  20. package/src/lib/calendar/business-calendar.provider.d.ts +8 -4
  21. package/src/lib/calendar/business-calendar.provider.js +15 -6
  22. package/src/lib/calendar/business-calendar.provider.js.map +1 -1
  23. package/src/lib/common/outbox.d.ts +26 -0
  24. package/src/lib/common/outbox.js +64 -0
  25. package/src/lib/common/outbox.js.map +1 -0
  26. package/src/lib/delegation/delegation.service.js +7 -7
  27. package/src/lib/delegation/delegation.service.js.map +1 -1
  28. package/src/lib/form-data-source/form-data-source.module.js +2 -4
  29. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  30. package/src/lib/form-data-source/form-data-source.provider.d.ts +14 -0
  31. package/src/lib/form-data-source/form-data-source.provider.js +23 -0
  32. package/src/lib/form-data-source/form-data-source.provider.js.map +1 -0
  33. package/src/lib/form-data-source/index.d.ts +1 -0
  34. package/src/lib/form-data-source/index.js +1 -0
  35. package/src/lib/form-data-source/index.js.map +1 -1
  36. package/src/lib/identity/default-member-resolver.d.ts +39 -0
  37. package/src/lib/identity/default-member-resolver.js +81 -0
  38. package/src/lib/identity/default-member-resolver.js.map +1 -0
  39. package/src/lib/identity/identity.module.d.ts +12 -2
  40. package/src/lib/identity/identity.module.js +2 -1
  41. package/src/lib/identity/identity.module.js.map +1 -1
  42. package/src/lib/identity/index.d.ts +1 -0
  43. package/src/lib/identity/index.js +1 -0
  44. package/src/lib/identity/index.js.map +1 -1
  45. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.d.ts +17 -0
  46. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.js +61 -0
  47. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.js.map +1 -0
  48. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.d.ts +16 -0
  49. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.js +71 -0
  50. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.js.map +1 -0
  51. package/src/lib/migrations/index.d.ts +2 -0
  52. package/src/lib/migrations/index.js +6 -0
  53. package/src/lib/migrations/index.js.map +1 -1
  54. package/src/lib/notification/notification-delivery.service.js +4 -57
  55. package/src/lib/notification/notification-delivery.service.js.map +1 -1
  56. package/src/lib/notification/notification.service.js +4 -3
  57. package/src/lib/notification/notification.service.js.map +1 -1
  58. package/src/lib/template/template.service.d.ts +9 -0
  59. package/src/lib/template/template.service.js +31 -0
  60. package/src/lib/template/template.service.js.map +1 -1
  61. package/src/lib/template/workflow-definition.validator.js +54 -1
  62. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  63. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +3 -1
  64. package/src/lib/workflow-engine/workflow-engine.enums.js +2 -0
  65. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  66. package/src/lib/workflow-engine/workflow-engine.module.js +3 -0
  67. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  68. package/src/lib/workflow-engine/workflow-engine.service.d.ts +5 -2
  69. package/src/lib/workflow-engine/workflow-engine.service.js +142 -26
  70. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  71. package/src/lib/workflow-webhook/index.d.ts +24 -0
  72. package/src/lib/workflow-webhook/index.js +28 -0
  73. package/src/lib/workflow-webhook/index.js.map +1 -0
  74. package/src/lib/workflow-webhook/workflow-webhook-allowlist.d.ts +38 -0
  75. package/src/lib/workflow-webhook/workflow-webhook-allowlist.js +296 -0
  76. package/src/lib/workflow-webhook/workflow-webhook-allowlist.js.map +1 -0
  77. package/src/lib/workflow-webhook/workflow-webhook-database-source.d.ts +33 -0
  78. package/src/lib/workflow-webhook/workflow-webhook-database-source.js +129 -0
  79. package/src/lib/workflow-webhook/workflow-webhook-database-source.js.map +1 -0
  80. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.d.ts +25 -0
  81. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.js +92 -0
  82. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.js.map +1 -0
  83. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.d.ts +53 -0
  84. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.js +97 -0
  85. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.js.map +1 -0
  86. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.d.ts +28 -0
  87. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.js +36 -0
  88. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.js.map +1 -0
  89. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.d.ts +28 -0
  90. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.js +88 -0
  91. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.js.map +1 -0
  92. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.d.ts +9 -0
  93. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.js +66 -0
  94. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.js.map +1 -0
  95. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +116 -0
  96. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +705 -0
  97. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -0
  98. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.d.ts +23 -0
  99. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.js +85 -0
  100. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.js.map +1 -0
  101. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.d.ts +85 -0
  102. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.js +345 -0
  103. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.js.map +1 -0
  104. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.d.ts +23 -0
  105. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.js +159 -0
  106. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.js.map +1 -0
  107. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.d.ts +93 -0
  108. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.js +464 -0
  109. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.js.map +1 -0
  110. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.d.ts +52 -0
  111. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.js +135 -0
  112. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.js.map +1 -0
  113. package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +46 -0
  114. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +134 -0
  115. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -0
  116. package/src/lib/workflow-webhook/workflow-webhook-options.d.ts +86 -0
  117. package/src/lib/workflow-webhook/workflow-webhook-options.js +88 -0
  118. package/src/lib/workflow-webhook/workflow-webhook-options.js.map +1 -0
  119. package/src/lib/workflow-webhook/workflow-webhook-options.module.d.ts +18 -0
  120. package/src/lib/workflow-webhook/workflow-webhook-options.module.js +57 -0
  121. package/src/lib/workflow-webhook/workflow-webhook-options.module.js.map +1 -0
  122. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.d.ts +20 -0
  123. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.js +66 -0
  124. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.js.map +1 -0
  125. package/src/lib/workflow-webhook/workflow-webhook.errors.d.ts +35 -0
  126. package/src/lib/workflow-webhook/workflow-webhook.errors.js +41 -0
  127. package/src/lib/workflow-webhook/workflow-webhook.errors.js.map +1 -0
  128. package/src/lib/workflow-webhook/workflow-webhook.module.d.ts +9 -0
  129. package/src/lib/workflow-webhook/workflow-webhook.module.js +62 -0
  130. package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -0
  131. package/src/lib/workflow-webhook/workflow-webhook.provider.d.ts +12 -0
  132. package/src/lib/workflow-webhook/workflow-webhook.provider.js +21 -0
  133. package/src/lib/workflow-webhook/workflow-webhook.provider.js.map +1 -0
  134. package/src/lib/workflow-webhook/workflow-webhook.queries.d.ts +38 -0
  135. package/src/lib/workflow-webhook/workflow-webhook.queries.js +142 -0
  136. package/src/lib/workflow-webhook/workflow-webhook.queries.js.map +1 -0
  137. package/src/lib/workflow-webhook/workflow-webhook.service.d.ts +50 -0
  138. package/src/lib/workflow-webhook/workflow-webhook.service.js +221 -0
  139. package/src/lib/workflow-webhook/workflow-webhook.service.js.map +1 -0
  140. package/src/lib/workflow-webhook/workflow-webhook.types.d.ts +127 -0
  141. package/src/lib/workflow-webhook/workflow-webhook.types.js +33 -0
  142. package/src/lib/workflow-webhook/workflow-webhook.types.js.map +1 -0
  143. package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +20 -0
  144. package/src/lib/workflow-webhook/workflow-webhook.validator.js +97 -0
  145. package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -0
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkflowEngineService = exports.MANUAL_TRANSFER_DELEGATION_REASON = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const form_1 = require("@rytass/bpm-core-shared/form");
6
+ const workflow_graph_1 = require("@rytass/bpm-core-shared/workflow-graph");
7
+ const workflow_webhook_delivery_service_1 = require("../workflow-webhook/workflow-webhook-delivery.service");
6
8
  const common_1 = require("@nestjs/common");
7
9
  const typeorm_1 = require("@nestjs/typeorm");
8
10
  const typeorm_2 = require("typeorm");
@@ -37,6 +39,7 @@ const workflow_condition_evaluator_1 = require("./workflow-condition-evaluator")
37
39
  const workflow_dry_run_object_1 = require("./workflow-dry-run.object");
38
40
  const workflow_service_task_dispatcher_token_1 = require("./workflow-service-task-dispatcher.token");
39
41
  const workflow_token_entity_1 = require("./workflow-token.entity");
42
+ const bpm_root_options_1 = require("../bpm/bpm-root-options");
40
43
  const MAX_PROCESSING_STEPS = 500;
41
44
  const DEFAULT_APPROVAL_HISTORY_TASK_LIMIT = 50;
42
45
  const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
@@ -52,7 +55,7 @@ const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
52
55
  'workflow.read_all',
53
56
  ]);
54
57
  let WorkflowEngineService = class WorkflowEngineService {
55
- constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, serviceTaskDispatcher = new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher(), formDataSourceValueResolver) {
58
+ constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, injectedServiceTaskDispatcher, formDataSourceValueResolver, rootOptions, webhookDeliveryService) {
56
59
  this.approvalInstanceRepository = approvalInstanceRepository;
57
60
  this.workflowTokenRepository = workflowTokenRepository;
58
61
  this.taskRepository = taskRepository;
@@ -70,8 +73,22 @@ let WorkflowEngineService = class WorkflowEngineService {
70
73
  this.notificationService = notificationService;
71
74
  this.signatureService = signatureService;
72
75
  this.slaScheduleService = slaScheduleService;
73
- this.serviceTaskDispatcher = serviceTaskDispatcher;
74
76
  this.formDataSourceValueResolver = formDataSourceValueResolver;
77
+ this.webhookDeliveryService = webhookDeliveryService;
78
+ // A binding under the token wins: that is either the host's own provider
79
+ // or `workflowServiceTaskDispatcherProvider`, both of them deliberate. The
80
+ // runtime instance from `BPMRootModule`'s async factory comes next.
81
+ //
82
+ // Resolving it here rather than registering a default provider is what
83
+ // keeps a host that binds the token from its own global module working —
84
+ // a module-local default would shadow that binding, silently sending
85
+ // WEBHOOK service tasks through the built-in `fetch` dispatcher instead of
86
+ // the host's signing or queueing one. It also reaches every copy of this
87
+ // service, which a provider registered on one module does not.
88
+ this.serviceTaskDispatcher =
89
+ injectedServiceTaskDispatcher ??
90
+ rootOptions?.workflowServiceTaskDispatcher ??
91
+ new workflow_service_task_dispatcher_token_1.DefaultWorkflowServiceTaskDispatcher();
75
92
  }
76
93
  async submitApprovalInstance(input, authContext) {
77
94
  const formData = parseJsonObject(input.formDataJson, 'formDataJson');
@@ -472,11 +489,13 @@ let WorkflowEngineService = class WorkflowEngineService {
472
489
  instanceId: instance.id,
473
490
  manager,
474
491
  });
475
- const cancelledInstance = await instanceRepository.save({
476
- ...instance,
492
+ // Same prototype-preserving reason as `cancelAdhocDirective`: this
493
+ // entity is returned to the resolver and `ApprovalInstance` exposes
494
+ // five getter-backed JSON fields.
495
+ const cancelledInstance = await instanceRepository.save(Object.assign(new approval_instance_entity_1.ApprovalInstanceEntity(), instance, {
477
496
  completedAt: cancelledAt,
478
497
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED,
479
- });
498
+ }));
480
499
  await this.dispatchAdhocCompletionNotifications(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED);
481
500
  await this.cancelPendingAdhocDirectives(manager, instance.id, null);
482
501
  await manager.getRepository(activity_log_entity_1.ActivityLogEntity).save(manager.getRepository(activity_log_entity_1.ActivityLogEntity).create({
@@ -660,10 +679,14 @@ let WorkflowEngineService = class WorkflowEngineService {
660
679
  if (directive.status !== adhoc_enums_1.AdhocDirectiveStatusEnum.PENDING) {
661
680
  throw new common_1.ConflictException(`Ad-hoc directive ${directive.id} is not pending`);
662
681
  }
663
- const cancelledDirective = await directiveRepository.save({
664
- ...directive,
682
+ // Assigning onto a fresh entity keeps the prototype, so the row handed
683
+ // back to the resolver still carries the getters the GraphQL schema
684
+ // exposes (e.g. `targetValueJson`). Spreading it into a plain object
685
+ // drops them, and the mutation then fails with an opaque
686
+ // INTERNAL_SERVER_ERROR *after* the cancellation has been committed.
687
+ const cancelledDirective = await directiveRepository.save(Object.assign(new adhoc_directive_entity_1.AdhocDirectiveEntity(), directive, {
665
688
  status: adhoc_enums_1.AdhocDirectiveStatusEnum.CANCELLED,
666
- });
689
+ }));
667
690
  await this.recordAdhocDirectiveActivity(manager, cancelledDirective, workflow_engine_enums_1.ActivityLogEventTypeEnum.ADHOC_DIRECTIVE_CANCELLED, cancelledByMemberId);
668
691
  return cancelledDirective;
669
692
  });
@@ -764,7 +787,18 @@ let WorkflowEngineService = class WorkflowEngineService {
764
787
  : workflow_engine_enums_1.TaskAssignmentTypeEnum.CANDIDATE_GROUP,
765
788
  completedAt: null,
766
789
  createdAt: now,
767
- decisionPolicySnapshot: { type: 'SINGLE' },
790
+ // One resolved person is `SINGLE`, which is what this has always been.
791
+ // Several mean a countersign in the sense the UI promises — 下一層需
792
+ // 所有人都完成才會繼續 — so every candidate has to decide before the
793
+ // task closes. Until now a multi-person target produced a single
794
+ // `SINGLE` task, and whoever opened it first decided for the whole
795
+ // group while the task vanished from everybody else's inbox.
796
+ // Rejection is unaffected: `shouldCompleteTaskAfterDecision` closes
797
+ // the task on any non-approval whatever the policy, and that rejection
798
+ // then rejects the instance.
799
+ decisionPolicySnapshot: candidates.length === 1
800
+ ? { type: 'SINGLE' }
801
+ : { type: 'PARALLEL_ALL' },
768
802
  delegationChain: candidates.length === 1 ? primaryCandidate.delegationChain : [],
769
803
  instanceId: instance.id,
770
804
  isAdhoc: true,
@@ -1220,6 +1254,12 @@ let WorkflowEngineService = class WorkflowEngineService {
1220
1254
  stepIndex: input.steps.length,
1221
1255
  }),
1222
1256
  ];
1257
+ // A NOTIFY node is an asynchronous branch: at runtime it consumes its
1258
+ // token and never advances, so its branch ends here rather than failing
1259
+ // for want of an outgoing edge it is not allowed to have.
1260
+ if ((0, workflow_graph_1.isAsyncNotifyServiceTask)(node)) {
1261
+ return nextSteps;
1262
+ }
1223
1263
  if (!entryConditionMet || node.type === 'endEvent') {
1224
1264
  if (node.type === 'endEvent') {
1225
1265
  return nextSteps;
@@ -1525,7 +1565,29 @@ let WorkflowEngineService = class WorkflowEngineService {
1525
1565
  },
1526
1566
  })
1527
1567
  : [];
1528
- return this.attachTaskCandidateSummaries(uniqueTasksById([...directTasks, ...candidateTasks]));
1568
+ const openTasks = uniqueTasksById([...directTasks, ...candidateTasks]);
1569
+ // Deciding on a group task writes the decider into `assigneeMemberId`, so
1570
+ // a task that stays open for the others — a `PARALLEL_ALL` countersign —
1571
+ // keeps matching `directTasks` for somebody who has already voted. Their
1572
+ // own candidate row is the authority: once it is COMPLETED the task is no
1573
+ // longer theirs to act on, and opening it again only raises "was already
1574
+ // decided by this member".
1575
+ //
1576
+ // Scoped to the tasks already in hand: unscoped it would load every row
1577
+ // this member has ever settled, on every inbox load. TRANSFERRED is not
1578
+ // among the statuses because transferring sets the task itself to
1579
+ // TRANSFERRED, which both queries above already exclude.
1580
+ const settledCandidateRows = openTasks.length
1581
+ ? await this.taskCandidateRepository.find({
1582
+ where: {
1583
+ memberId: assigneeMemberId,
1584
+ status: workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED,
1585
+ taskId: (0, typeorm_2.In)(openTasks.map((task) => task.id)),
1586
+ },
1587
+ })
1588
+ : [];
1589
+ const settledTaskIds = new Set(settledCandidateRows.map((candidate) => candidate.taskId));
1590
+ return this.attachTaskCandidateSummaries(openTasks.filter((task) => !settledTaskIds.has(task.id)));
1529
1591
  }
1530
1592
  async listApprovalHistoryTasks(assigneeMemberId) {
1531
1593
  const directTasks = await this.taskRepository.find({
@@ -1539,13 +1601,17 @@ let WorkflowEngineService = class WorkflowEngineService {
1539
1601
  status: workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED,
1540
1602
  },
1541
1603
  });
1604
+ // No status filter on this branch: the candidate row is already COMPLETED,
1605
+ // which means this member has cast their decision. Requiring the task
1606
+ // itself to be COMPLETED hid their own vote for as long as a multi-person
1607
+ // countersign waited on somebody else — and the inbox has, correctly,
1608
+ // stopped showing it to them by then, so it appeared nowhere at all.
1542
1609
  const candidateTasks = candidateRows.length
1543
1610
  ? await this.taskRepository.find({
1544
1611
  order: { completedAt: 'DESC', createdAt: 'DESC' },
1545
1612
  take: DEFAULT_APPROVAL_HISTORY_TASK_LIMIT,
1546
1613
  where: {
1547
1614
  id: (0, typeorm_2.In)(candidateRows.map((candidate) => candidate.taskId)),
1548
- status: workflow_engine_enums_1.TaskStatusEnum.COMPLETED,
1549
1615
  },
1550
1616
  })
1551
1617
  : [];
@@ -1709,25 +1775,39 @@ let WorkflowEngineService = class WorkflowEngineService {
1709
1775
  if (instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
1710
1776
  return;
1711
1777
  }
1712
- const waitingTokens = await manager
1713
- .getRepository(workflow_token_entity_1.WorkflowTokenEntity)
1714
- .find({
1778
+ // Both open statuses, not just WAITING. Today this is reached only from
1779
+ // `processRunningInstance`, whose loop has already established that no
1780
+ // ACTIVE token remains, so the extra status changes no current behaviour —
1781
+ // but nothing enforces that invariant, and a second caller arriving
1782
+ // without it would complete a case while somebody still had a task open.
1783
+ const openTokens = await manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity).find({
1715
1784
  where: {
1716
1785
  instanceId: instance.id,
1717
- status: workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING,
1786
+ status: (0, typeorm_2.In)([
1787
+ workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE,
1788
+ workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING,
1789
+ ]),
1718
1790
  },
1719
1791
  });
1720
- if (waitingTokens.length > 0) {
1792
+ if (openTokens.length > 0) {
1721
1793
  return;
1722
1794
  }
1723
1795
  const completedAt = new Date();
1796
+ // Update the tracked entity in place instead of saving a copy of it. The
1797
+ // caller still holds this object and may return it — `submitApprovalInstance`
1798
+ // hands it straight back to the resolver — so writing a copy left the
1799
+ // mutation reporting `RUNNING` with a null `completedAt` on a case the same
1800
+ // transaction had already committed as `APPROVED`. Assigning in place also
1801
+ // preserves whatever the caller handed in, so an entity stays an entity
1802
+ // and its getter-backed `*Json` fields survive. It cannot upgrade a plain
1803
+ // object, and the resubmit and return paths do pass a plain copy of their
1804
+ // own — a separate problem this does not fix.
1724
1805
  const completedInstance = await manager
1725
1806
  .getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
1726
- .save({
1727
- ...instance,
1807
+ .save(Object.assign(instance, {
1728
1808
  completedAt,
1729
1809
  state: workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED,
1730
- });
1810
+ }));
1731
1811
  await this.notificationService.createInstanceCompletedNotification({
1732
1812
  instance: completedInstance,
1733
1813
  manager,
@@ -1957,11 +2037,14 @@ let WorkflowEngineService = class WorkflowEngineService {
1957
2037
  (candidate.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.ACTIVE ||
1958
2038
  candidate.status === workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING));
1959
2039
  if (!hasOpenToken) {
1960
- const completedInstance = await instanceRepository.save({
1961
- ...instance,
1962
- completedAt,
1963
- state: instanceState,
1964
- });
2040
+ // Update the tracked entity in place. Saving a copy left the caller's
2041
+ // `instance.state` on `RUNNING`, so the `state !== RUNNING` guard in
2042
+ // `completeInstanceIfNoOpenRuntimeState` did not short-circuit and a
2043
+ // *second* completion ran on the way out of `processRunningInstance`:
2044
+ // a duplicate completion notification on every approved case, and — far
2045
+ // worse — an `endState: 'REJECTED'` row overwritten with `APPROVED`
2046
+ // immediately after it had been committed.
2047
+ const completedInstance = await instanceRepository.save(Object.assign(instance, { completedAt, state: instanceState }));
1965
2048
  if (instanceState === workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED) {
1966
2049
  await this.notificationService.createInstanceCompletedNotification({
1967
2050
  instance: completedInstance,
@@ -2533,7 +2616,13 @@ let WorkflowEngineService = class WorkflowEngineService {
2533
2616
  await this.advanceTokenToOutgoingNodes(manager, updatedInstance, token, node);
2534
2617
  return;
2535
2618
  }
2536
- const recipients = await this.resolveApproverResolver(manager, instance, action.recipients, `知會節點「${node.data.label}」`);
2619
+ // A webhook-only NOTIFY node names nobody. Resolving its empty DIRECT
2620
+ // resolver would throw and roll back the submit or decision that reached
2621
+ // it, so skip member resolution; webhooks are delivered separately.
2622
+ const recipients = (0, workflow_graph_1.isNotifyRecipientsEmpty)(action.recipients) &&
2623
+ (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
2624
+ ? []
2625
+ : await this.resolveApproverResolver(manager, instance, action.recipients, `知會節點「${node.data.label}」`);
2537
2626
  const recipientMemberIds = uniqueTexts(recipients.map((recipient) => recipient.memberId));
2538
2627
  await this.notificationService.createServiceTaskNotifications({
2539
2628
  instance,
@@ -2541,6 +2630,26 @@ let WorkflowEngineService = class WorkflowEngineService {
2541
2630
  node,
2542
2631
  recipientMemberIds,
2543
2632
  });
2633
+ // Queued in this transaction and sent only after it commits (ADR 18
2634
+ // §3.5). Without the webhook module there is nothing to queue into; the
2635
+ // publish lint already refuses a template that references an endpoint
2636
+ // in that case.
2637
+ const webhookDeliveryIds = this.webhookDeliveryService &&
2638
+ (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
2639
+ ? await this.webhookDeliveryService.enqueueNotifyWebhooks(manager, {
2640
+ instance: {
2641
+ formData: instance.formData,
2642
+ id: instance.id,
2643
+ initiatorMemberId: instance.initiatorMemberId,
2644
+ templateId: instance.templateId,
2645
+ templateVersionId: instance.templateVersionId,
2646
+ title: instance.title,
2647
+ },
2648
+ node: { id: node.id, label: node.data.label },
2649
+ occurredAt: new Date(),
2650
+ tokenId: token.id,
2651
+ }, action.webhooks)
2652
+ : [];
2544
2653
  await manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity).save({
2545
2654
  ...token,
2546
2655
  consumedAt: new Date(),
@@ -2555,6 +2664,7 @@ let WorkflowEngineService = class WorkflowEngineService {
2555
2664
  action: 'NOTIFY',
2556
2665
  recipientMemberIds,
2557
2666
  tokenId: token.id,
2667
+ ...(webhookDeliveryIds.length ? { webhookDeliveryIds } : {}),
2558
2668
  },
2559
2669
  taskId: null,
2560
2670
  }));
@@ -2844,6 +2954,9 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2844
2954
  tslib_1.__param(17, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2845
2955
  tslib_1.__param(18, (0, common_1.Optional)()),
2846
2956
  tslib_1.__param(18, (0, common_1.Inject)(form_data_source_types_1.BPM_FORM_DATA_SOURCE_VALUE_RESOLVER)),
2957
+ tslib_1.__param(19, (0, common_1.Optional)()),
2958
+ tslib_1.__param(19, (0, common_1.Inject)(bpm_root_options_1.BPM_ROOT_OPTIONS)),
2959
+ tslib_1.__param(20, (0, common_1.Optional)()),
2847
2960
  tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
2848
2961
  typeorm_2.Repository,
2849
2962
  typeorm_2.Repository,
@@ -2860,7 +2973,7 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2860
2973
  delegation_service_1.DelegationService,
2861
2974
  notification_service_1.NotificationService,
2862
2975
  signature_service_1.SignatureService,
2863
- sla_schedule_service_1.BPMSlaScheduleService, Object, Object])
2976
+ sla_schedule_service_1.BPMSlaScheduleService, Object, Object, Object, workflow_webhook_delivery_service_1.WorkflowWebhookDeliveryService])
2864
2977
  ], WorkflowEngineService);
2865
2978
  function parseJsonObject(value, label) {
2866
2979
  try {
@@ -3936,6 +4049,9 @@ function readDryRunStepMessage(node, entryConditionMet) {
3936
4049
  if (node.type === 'endEvent') {
3937
4050
  return `流程結束:${node.data.endState ?? 'APPROVED'}`;
3938
4051
  }
4052
+ if ((0, workflow_graph_1.isAsyncNotifyServiceTask)(node)) {
4053
+ return '將發送知會,此分支不會繼續往下。';
4054
+ }
3939
4055
  return '節點條件通過。';
3940
4056
  }
3941
4057
  function mapDecisionToResolution(action) {