@rytass/bpm-core-nestjs-module 0.13.3 → 0.13.5

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 (143) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +14 -7
  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/bpm/bpm-root-options.d.ts +134 -0
  10. package/src/lib/bpm/bpm-root-options.js +61 -0
  11. package/src/lib/bpm/bpm-root-options.js.map +1 -0
  12. package/src/lib/bpm/bpm-root.module.d.ts +101 -137
  13. package/src/lib/bpm/bpm-root.module.js +83 -118
  14. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  15. package/src/lib/bpm/index.d.ts +1 -0
  16. package/src/lib/bpm/index.js +1 -0
  17. package/src/lib/bpm/index.js.map +1 -1
  18. package/src/lib/calendar/business-calendar.provider.d.ts +8 -4
  19. package/src/lib/calendar/business-calendar.provider.js +15 -6
  20. package/src/lib/calendar/business-calendar.provider.js.map +1 -1
  21. package/src/lib/common/outbox.d.ts +26 -0
  22. package/src/lib/common/outbox.js +64 -0
  23. package/src/lib/common/outbox.js.map +1 -0
  24. package/src/lib/delegation/delegation.service.js +7 -7
  25. package/src/lib/delegation/delegation.service.js.map +1 -1
  26. package/src/lib/form-data-source/form-data-source.module.js +2 -4
  27. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  28. package/src/lib/form-data-source/form-data-source.provider.d.ts +14 -0
  29. package/src/lib/form-data-source/form-data-source.provider.js +23 -0
  30. package/src/lib/form-data-source/form-data-source.provider.js.map +1 -0
  31. package/src/lib/form-data-source/index.d.ts +1 -0
  32. package/src/lib/form-data-source/index.js +1 -0
  33. package/src/lib/form-data-source/index.js.map +1 -1
  34. package/src/lib/identity/default-member-resolver.d.ts +39 -0
  35. package/src/lib/identity/default-member-resolver.js +81 -0
  36. package/src/lib/identity/default-member-resolver.js.map +1 -0
  37. package/src/lib/identity/identity.module.d.ts +12 -2
  38. package/src/lib/identity/identity.module.js +2 -1
  39. package/src/lib/identity/identity.module.js.map +1 -1
  40. package/src/lib/identity/index.d.ts +1 -0
  41. package/src/lib/identity/index.js +1 -0
  42. package/src/lib/identity/index.js.map +1 -1
  43. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.d.ts +17 -0
  44. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.js +61 -0
  45. package/src/lib/migrations/0000000023000-workflow-webhook-deliveries.js.map +1 -0
  46. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.d.ts +16 -0
  47. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.js +71 -0
  48. package/src/lib/migrations/0000000024000-workflow-webhook-endpoints.js.map +1 -0
  49. package/src/lib/migrations/index.d.ts +2 -0
  50. package/src/lib/migrations/index.js +6 -0
  51. package/src/lib/migrations/index.js.map +1 -1
  52. package/src/lib/notification/notification-delivery.service.js +4 -57
  53. package/src/lib/notification/notification-delivery.service.js.map +1 -1
  54. package/src/lib/notification/notification.service.js +4 -3
  55. package/src/lib/notification/notification.service.js.map +1 -1
  56. package/src/lib/template/template.service.d.ts +9 -0
  57. package/src/lib/template/template.service.js +31 -0
  58. package/src/lib/template/template.service.js.map +1 -1
  59. package/src/lib/template/workflow-definition.validator.js +54 -1
  60. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  61. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +3 -1
  62. package/src/lib/workflow-engine/workflow-engine.enums.js +2 -0
  63. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  64. package/src/lib/workflow-engine/workflow-engine.module.js +3 -0
  65. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  66. package/src/lib/workflow-engine/workflow-engine.service.d.ts +5 -2
  67. package/src/lib/workflow-engine/workflow-engine.service.js +150 -26
  68. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
  69. package/src/lib/workflow-webhook/index.d.ts +24 -0
  70. package/src/lib/workflow-webhook/index.js +28 -0
  71. package/src/lib/workflow-webhook/index.js.map +1 -0
  72. package/src/lib/workflow-webhook/workflow-webhook-allowlist.d.ts +38 -0
  73. package/src/lib/workflow-webhook/workflow-webhook-allowlist.js +296 -0
  74. package/src/lib/workflow-webhook/workflow-webhook-allowlist.js.map +1 -0
  75. package/src/lib/workflow-webhook/workflow-webhook-database-source.d.ts +33 -0
  76. package/src/lib/workflow-webhook/workflow-webhook-database-source.js +129 -0
  77. package/src/lib/workflow-webhook/workflow-webhook-database-source.js.map +1 -0
  78. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.d.ts +25 -0
  79. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.js +92 -0
  80. package/src/lib/workflow-webhook/workflow-webhook-delivery-scheduler.service.js.map +1 -0
  81. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.d.ts +53 -0
  82. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.js +97 -0
  83. package/src/lib/workflow-webhook/workflow-webhook-delivery.entity.js.map +1 -0
  84. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.d.ts +28 -0
  85. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.js +36 -0
  86. package/src/lib/workflow-webhook/workflow-webhook-delivery.enums.js.map +1 -0
  87. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.d.ts +28 -0
  88. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.js +88 -0
  89. package/src/lib/workflow-webhook/workflow-webhook-delivery.object.js.map +1 -0
  90. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.d.ts +9 -0
  91. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.js +66 -0
  92. package/src/lib/workflow-webhook/workflow-webhook-delivery.resolver.js.map +1 -0
  93. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +116 -0
  94. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +705 -0
  95. package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -0
  96. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.d.ts +23 -0
  97. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.js +85 -0
  98. package/src/lib/workflow-webhook/workflow-webhook-delivery.subscriber.js.map +1 -0
  99. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.d.ts +85 -0
  100. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.js +345 -0
  101. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.dto.js.map +1 -0
  102. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.d.ts +23 -0
  103. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.js +159 -0
  104. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.resolver.js.map +1 -0
  105. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.d.ts +93 -0
  106. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.js +464 -0
  107. package/src/lib/workflow-webhook/workflow-webhook-endpoint-admin.service.js.map +1 -0
  108. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.d.ts +52 -0
  109. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.js +135 -0
  110. package/src/lib/workflow-webhook/workflow-webhook-endpoint.entity.js.map +1 -0
  111. package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +46 -0
  112. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +134 -0
  113. package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -0
  114. package/src/lib/workflow-webhook/workflow-webhook-options.d.ts +86 -0
  115. package/src/lib/workflow-webhook/workflow-webhook-options.js +88 -0
  116. package/src/lib/workflow-webhook/workflow-webhook-options.js.map +1 -0
  117. package/src/lib/workflow-webhook/workflow-webhook-options.module.d.ts +18 -0
  118. package/src/lib/workflow-webhook/workflow-webhook-options.module.js +57 -0
  119. package/src/lib/workflow-webhook/workflow-webhook-options.module.js.map +1 -0
  120. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.d.ts +20 -0
  121. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.js +66 -0
  122. package/src/lib/workflow-webhook/workflow-webhook-secret-cipher.js.map +1 -0
  123. package/src/lib/workflow-webhook/workflow-webhook.errors.d.ts +35 -0
  124. package/src/lib/workflow-webhook/workflow-webhook.errors.js +41 -0
  125. package/src/lib/workflow-webhook/workflow-webhook.errors.js.map +1 -0
  126. package/src/lib/workflow-webhook/workflow-webhook.module.d.ts +9 -0
  127. package/src/lib/workflow-webhook/workflow-webhook.module.js +62 -0
  128. package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -0
  129. package/src/lib/workflow-webhook/workflow-webhook.provider.d.ts +12 -0
  130. package/src/lib/workflow-webhook/workflow-webhook.provider.js +21 -0
  131. package/src/lib/workflow-webhook/workflow-webhook.provider.js.map +1 -0
  132. package/src/lib/workflow-webhook/workflow-webhook.queries.d.ts +38 -0
  133. package/src/lib/workflow-webhook/workflow-webhook.queries.js +142 -0
  134. package/src/lib/workflow-webhook/workflow-webhook.queries.js.map +1 -0
  135. package/src/lib/workflow-webhook/workflow-webhook.service.d.ts +50 -0
  136. package/src/lib/workflow-webhook/workflow-webhook.service.js +221 -0
  137. package/src/lib/workflow-webhook/workflow-webhook.service.js.map +1 -0
  138. package/src/lib/workflow-webhook/workflow-webhook.types.d.ts +127 -0
  139. package/src/lib/workflow-webhook/workflow-webhook.types.js +33 -0
  140. package/src/lib/workflow-webhook/workflow-webhook.types.js.map +1 -0
  141. package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +20 -0
  142. package/src/lib/workflow-webhook/workflow-webhook.validator.js +97 -0
  143. 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,
@@ -2148,12 +2231,20 @@ let WorkflowEngineService = class WorkflowEngineService {
2148
2231
  status: workflow_engine_enums_1.TaskStatusEnum.TRANSFERRED,
2149
2232
  });
2150
2233
  const nextTask = await taskRepository.save(taskRepository.create({
2234
+ // A transferred ad-hoc task stays the same ad-hoc task for its new
2235
+ // holder: the pre-approval rejection path (RETURN_TO_ORIGIN) reads these
2236
+ // off the task being decided, so without them a rejection after a
2237
+ // transfer rejected the whole instance instead.
2238
+ adhocDirectiveId: task.adhocDirectiveId,
2239
+ adhocOriginTaskId: task.adhocOriginTaskId,
2240
+ adhocType: task.adhocType,
2151
2241
  assigneeMemberId: transferToMemberId,
2152
2242
  assignmentType: workflow_engine_enums_1.TaskAssignmentTypeEnum.DIRECT_MEMBER,
2153
2243
  completedAt: null,
2154
2244
  decisionPolicySnapshot: { type: 'SINGLE' },
2155
2245
  delegationChain: nextDelegationChain,
2156
2246
  instanceId: task.instanceId,
2247
+ isAdhoc: task.isAdhoc,
2157
2248
  nodeId: task.nodeId,
2158
2249
  openedAt: null,
2159
2250
  originalAssigneeMemberId: task.originalAssigneeMemberId,
@@ -2533,7 +2624,13 @@ let WorkflowEngineService = class WorkflowEngineService {
2533
2624
  await this.advanceTokenToOutgoingNodes(manager, updatedInstance, token, node);
2534
2625
  return;
2535
2626
  }
2536
- const recipients = await this.resolveApproverResolver(manager, instance, action.recipients, `知會節點「${node.data.label}」`);
2627
+ // A webhook-only NOTIFY node names nobody. Resolving its empty DIRECT
2628
+ // resolver would throw and roll back the submit or decision that reached
2629
+ // it, so skip member resolution; webhooks are delivered separately.
2630
+ const recipients = (0, workflow_graph_1.isNotifyRecipientsEmpty)(action.recipients) &&
2631
+ (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
2632
+ ? []
2633
+ : await this.resolveApproverResolver(manager, instance, action.recipients, `知會節點「${node.data.label}」`);
2537
2634
  const recipientMemberIds = uniqueTexts(recipients.map((recipient) => recipient.memberId));
2538
2635
  await this.notificationService.createServiceTaskNotifications({
2539
2636
  instance,
@@ -2541,6 +2638,26 @@ let WorkflowEngineService = class WorkflowEngineService {
2541
2638
  node,
2542
2639
  recipientMemberIds,
2543
2640
  });
2641
+ // Queued in this transaction and sent only after it commits (ADR 18
2642
+ // §3.5). Without the webhook module there is nothing to queue into; the
2643
+ // publish lint already refuses a template that references an endpoint
2644
+ // in that case.
2645
+ const webhookDeliveryIds = this.webhookDeliveryService &&
2646
+ (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
2647
+ ? await this.webhookDeliveryService.enqueueNotifyWebhooks(manager, {
2648
+ instance: {
2649
+ formData: instance.formData,
2650
+ id: instance.id,
2651
+ initiatorMemberId: instance.initiatorMemberId,
2652
+ templateId: instance.templateId,
2653
+ templateVersionId: instance.templateVersionId,
2654
+ title: instance.title,
2655
+ },
2656
+ node: { id: node.id, label: node.data.label },
2657
+ occurredAt: new Date(),
2658
+ tokenId: token.id,
2659
+ }, action.webhooks)
2660
+ : [];
2544
2661
  await manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity).save({
2545
2662
  ...token,
2546
2663
  consumedAt: new Date(),
@@ -2555,6 +2672,7 @@ let WorkflowEngineService = class WorkflowEngineService {
2555
2672
  action: 'NOTIFY',
2556
2673
  recipientMemberIds,
2557
2674
  tokenId: token.id,
2675
+ ...(webhookDeliveryIds.length ? { webhookDeliveryIds } : {}),
2558
2676
  },
2559
2677
  taskId: null,
2560
2678
  }));
@@ -2844,6 +2962,9 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2844
2962
  tslib_1.__param(17, (0, common_1.Inject)(workflow_service_task_dispatcher_token_1.BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)),
2845
2963
  tslib_1.__param(18, (0, common_1.Optional)()),
2846
2964
  tslib_1.__param(18, (0, common_1.Inject)(form_data_source_types_1.BPM_FORM_DATA_SOURCE_VALUE_RESOLVER)),
2965
+ tslib_1.__param(19, (0, common_1.Optional)()),
2966
+ tslib_1.__param(19, (0, common_1.Inject)(bpm_root_options_1.BPM_ROOT_OPTIONS)),
2967
+ tslib_1.__param(20, (0, common_1.Optional)()),
2847
2968
  tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
2848
2969
  typeorm_2.Repository,
2849
2970
  typeorm_2.Repository,
@@ -2860,7 +2981,7 @@ exports.WorkflowEngineService = WorkflowEngineService = tslib_1.__decorate([
2860
2981
  delegation_service_1.DelegationService,
2861
2982
  notification_service_1.NotificationService,
2862
2983
  signature_service_1.SignatureService,
2863
- sla_schedule_service_1.BPMSlaScheduleService, Object, Object])
2984
+ sla_schedule_service_1.BPMSlaScheduleService, Object, Object, Object, workflow_webhook_delivery_service_1.WorkflowWebhookDeliveryService])
2864
2985
  ], WorkflowEngineService);
2865
2986
  function parseJsonObject(value, label) {
2866
2987
  try {
@@ -3936,6 +4057,9 @@ function readDryRunStepMessage(node, entryConditionMet) {
3936
4057
  if (node.type === 'endEvent') {
3937
4058
  return `流程結束:${node.data.endState ?? 'APPROVED'}`;
3938
4059
  }
4060
+ if ((0, workflow_graph_1.isAsyncNotifyServiceTask)(node)) {
4061
+ return '將發送知會,此分支不會繼續往下。';
4062
+ }
3939
4063
  return '節點條件通過。';
3940
4064
  }
3941
4065
  function mapDecisionToResolution(action) {