@rytass/bpm-core-nestjs-module 0.1.3 → 0.1.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 (612) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +143 -0
  4. package/package.json +3 -3
  5. package/src/index.js +18 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/attachment/attachment-options.d.ts +44 -0
  8. package/src/lib/attachment/attachment-options.js +62 -0
  9. package/src/lib/attachment/attachment-options.js.map +1 -0
  10. package/src/lib/attachment/attachment-storage.provider.d.ts +4 -0
  11. package/src/lib/attachment/attachment-storage.provider.js +35 -0
  12. package/src/lib/attachment/attachment-storage.provider.js.map +1 -0
  13. package/src/lib/attachment/{attachment-storage.token.ts → attachment-storage.token.d.ts} +1 -3
  14. package/src/lib/attachment/attachment-storage.token.js +5 -0
  15. package/src/lib/attachment/attachment-storage.token.js.map +1 -0
  16. package/src/lib/attachment/attachment.controller.d.ts +8 -0
  17. package/src/lib/attachment/attachment.controller.js +38 -0
  18. package/src/lib/attachment/attachment.controller.js.map +1 -0
  19. package/src/lib/attachment/attachment.entity.d.ts +14 -0
  20. package/src/lib/attachment/attachment.entity.js +74 -0
  21. package/src/lib/attachment/attachment.entity.js.map +1 -0
  22. package/src/lib/attachment/attachment.module.d.ts +27 -0
  23. package/src/lib/attachment/attachment.module.js +103 -0
  24. package/src/lib/attachment/attachment.module.js.map +1 -0
  25. package/src/lib/attachment/attachment.mutations.d.ts +8 -0
  26. package/src/lib/attachment/attachment.mutations.js +35 -0
  27. package/src/lib/attachment/attachment.mutations.js.map +1 -0
  28. package/src/lib/attachment/attachment.queries.d.ts +9 -0
  29. package/src/lib/attachment/attachment.queries.js +78 -0
  30. package/src/lib/attachment/attachment.queries.js.map +1 -0
  31. package/src/lib/attachment/attachment.service.d.ts +49 -0
  32. package/src/lib/attachment/attachment.service.js +276 -0
  33. package/src/lib/attachment/attachment.service.js.map +1 -0
  34. package/src/lib/attachment/dto/upload-attachment.input.d.ts +11 -0
  35. package/src/lib/attachment/dto/upload-attachment.input.js +68 -0
  36. package/src/lib/attachment/dto/upload-attachment.input.js.map +1 -0
  37. package/src/lib/attachment/index.js +9 -0
  38. package/src/lib/attachment/index.js.map +1 -0
  39. package/src/lib/bpm/bpm-root.module.d.ts +155 -0
  40. package/src/lib/bpm/bpm-root.module.js +170 -0
  41. package/src/lib/bpm/bpm-root.module.js.map +1 -0
  42. package/src/lib/bpm/index.js +5 -0
  43. package/src/lib/bpm/index.js.map +1 -0
  44. package/src/lib/bpm-auth/bpm-auth-context.d.ts +24 -0
  45. package/src/lib/bpm-auth/bpm-auth-context.extractor.d.ts +4 -0
  46. package/src/lib/bpm-auth/bpm-auth-context.extractor.js +69 -0
  47. package/src/lib/bpm-auth/bpm-auth-context.extractor.js.map +1 -0
  48. package/src/lib/bpm-auth/bpm-auth-context.js +15 -0
  49. package/src/lib/bpm-auth/bpm-auth-context.js.map +1 -0
  50. package/src/lib/bpm-auth/bpm-auth.authorization.d.ts +12 -0
  51. package/src/lib/bpm-auth/bpm-auth.authorization.js +67 -0
  52. package/src/lib/bpm-auth/bpm-auth.authorization.js.map +1 -0
  53. package/src/lib/bpm-auth/bpm-auth.decorators.d.ts +3 -0
  54. package/src/lib/bpm-auth/bpm-auth.decorators.js +11 -0
  55. package/src/lib/bpm-auth/bpm-auth.decorators.js.map +1 -0
  56. package/src/lib/bpm-auth/bpm-auth.guard.d.ts +7 -0
  57. package/src/lib/bpm-auth/bpm-auth.guard.js +24 -0
  58. package/src/lib/bpm-auth/bpm-auth.guard.js.map +1 -0
  59. package/src/lib/bpm-auth/bpm-auth.module.d.ts +6 -0
  60. package/src/lib/bpm-auth/bpm-auth.module.js +69 -0
  61. package/src/lib/bpm-auth/bpm-auth.module.js.map +1 -0
  62. package/src/lib/bpm-auth/bpm-auth.options.d.ts +11 -0
  63. package/src/lib/bpm-auth/bpm-auth.options.js +3 -0
  64. package/src/lib/bpm-auth/bpm-auth.options.js.map +1 -0
  65. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.d.ts +10 -0
  66. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js +40 -0
  67. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js.map +1 -0
  68. package/src/lib/bpm-auth/index.js +10 -0
  69. package/src/lib/bpm-auth/index.js.map +1 -0
  70. package/src/lib/common/filters/all-exceptions.filter.d.ts +25 -0
  71. package/src/lib/common/filters/all-exceptions.filter.js +93 -0
  72. package/src/lib/common/filters/all-exceptions.filter.js.map +1 -0
  73. package/src/lib/common/index.js +5 -0
  74. package/src/lib/common/index.js.map +1 -0
  75. package/src/lib/common/iso-duration.d.ts +1 -0
  76. package/src/lib/common/iso-duration.js +27 -0
  77. package/src/lib/common/iso-duration.js.map +1 -0
  78. package/src/lib/condition/condition.module.d.ts +2 -0
  79. package/src/lib/condition/condition.module.js +16 -0
  80. package/src/lib/condition/condition.module.js.map +1 -0
  81. package/src/lib/condition/condition.service.d.ts +16 -0
  82. package/src/lib/condition/condition.service.js +82 -0
  83. package/src/lib/condition/condition.service.js.map +1 -0
  84. package/src/lib/condition/index.js +6 -0
  85. package/src/lib/condition/index.js.map +1 -0
  86. package/src/lib/database/data-source.d.ts +4 -0
  87. package/src/lib/database/data-source.js +7 -0
  88. package/src/lib/database/data-source.js.map +1 -0
  89. package/src/lib/database/index.js +6 -0
  90. package/src/lib/database/index.js.map +1 -0
  91. package/src/lib/database/migration-runner.d.ts +1 -0
  92. package/src/lib/database/migration-runner.js +23 -0
  93. package/src/lib/database/migration-runner.js.map +1 -0
  94. package/src/lib/database/typeorm.config.d.ts +14 -0
  95. package/src/lib/database/typeorm.config.js +135 -0
  96. package/src/lib/database/typeorm.config.js.map +1 -0
  97. package/src/lib/delegation/delegation-rule.entity.d.ts +19 -0
  98. package/src/lib/delegation/delegation-rule.entity.js +99 -0
  99. package/src/lib/delegation/delegation-rule.entity.js.map +1 -0
  100. package/src/lib/delegation/delegation.enums.d.ts +10 -0
  101. package/src/lib/delegation/delegation.enums.js +23 -0
  102. package/src/lib/delegation/delegation.enums.js.map +1 -0
  103. package/src/lib/delegation/delegation.module.d.ts +2 -0
  104. package/src/lib/delegation/delegation.module.js +22 -0
  105. package/src/lib/delegation/delegation.module.js.map +1 -0
  106. package/src/lib/delegation/delegation.mutations.d.ts +11 -0
  107. package/src/lib/delegation/delegation.mutations.js +87 -0
  108. package/src/lib/delegation/delegation.mutations.js.map +1 -0
  109. package/src/lib/delegation/delegation.queries.d.ts +10 -0
  110. package/src/lib/delegation/delegation.queries.js +91 -0
  111. package/src/lib/delegation/delegation.queries.js.map +1 -0
  112. package/src/lib/delegation/delegation.service.d.ts +56 -0
  113. package/src/lib/delegation/delegation.service.js +268 -0
  114. package/src/lib/delegation/delegation.service.js.map +1 -0
  115. package/src/lib/delegation/dto/delegation-rule.input.d.ts +24 -0
  116. package/src/lib/delegation/dto/delegation-rule.input.js +143 -0
  117. package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -0
  118. package/src/lib/delegation/index.js +9 -0
  119. package/src/lib/delegation/index.js.map +1 -0
  120. package/src/lib/form/dto/form-definition.input.d.ts +21 -0
  121. package/src/lib/form/dto/form-definition.input.js +103 -0
  122. package/src/lib/form/dto/form-definition.input.js.map +1 -0
  123. package/src/lib/form/form-definition-version.entity.d.ts +17 -0
  124. package/src/lib/form/form-definition-version.entity.js +84 -0
  125. package/src/lib/form/form-definition-version.entity.js.map +1 -0
  126. package/src/lib/form/form-definition.entity.d.ts +13 -0
  127. package/src/lib/form/form-definition.entity.js +66 -0
  128. package/src/lib/form/form-definition.entity.js.map +1 -0
  129. package/src/lib/form/form-schema-lint.object.d.ts +4 -0
  130. package/src/lib/form/form-schema-lint.object.js +20 -0
  131. package/src/lib/form/form-schema-lint.object.js.map +1 -0
  132. package/src/lib/form/form-schema.validator.d.ts +13 -0
  133. package/src/lib/form/form-schema.validator.js +354 -0
  134. package/src/lib/form/form-schema.validator.js.map +1 -0
  135. package/src/lib/form/form.enums.d.ts +9 -0
  136. package/src/lib/form/form.enums.js +22 -0
  137. package/src/lib/form/form.enums.js.map +1 -0
  138. package/src/lib/form/form.module.d.ts +2 -0
  139. package/src/lib/form/form.module.js +27 -0
  140. package/src/lib/form/form.module.js.map +1 -0
  141. package/src/lib/form/form.mutations.d.ts +14 -0
  142. package/src/lib/form/form.mutations.js +88 -0
  143. package/src/lib/form/form.mutations.js.map +1 -0
  144. package/src/lib/form/form.queries.d.ts +16 -0
  145. package/src/lib/form/form.queries.js +96 -0
  146. package/src/lib/form/form.queries.js.map +1 -0
  147. package/src/lib/form/form.service.d.ts +35 -0
  148. package/src/lib/form/form.service.js +297 -0
  149. package/src/lib/form/form.service.js.map +1 -0
  150. package/src/lib/form/index.js +12 -0
  151. package/src/lib/form/index.js.map +1 -0
  152. package/src/lib/identity/identity-options.d.ts +13 -0
  153. package/src/lib/identity/identity-options.js +20 -0
  154. package/src/lib/identity/identity-options.js.map +1 -0
  155. package/src/lib/identity/identity.module.d.ts +17 -0
  156. package/src/lib/identity/identity.module.js +65 -0
  157. package/src/lib/identity/identity.module.js.map +1 -0
  158. package/src/lib/identity/identity.queries.d.ts +12 -0
  159. package/src/lib/identity/identity.queries.js +79 -0
  160. package/src/lib/identity/identity.queries.js.map +1 -0
  161. package/src/lib/identity/identity.service.d.ts +22 -0
  162. package/src/lib/identity/identity.service.js +141 -0
  163. package/src/lib/identity/identity.service.js.map +1 -0
  164. package/src/lib/identity/index.js +7 -0
  165. package/src/lib/identity/index.js.map +1 -0
  166. package/src/lib/identity/member-base.adapter.d.ts +95 -0
  167. package/src/lib/identity/member-base.adapter.js +137 -0
  168. package/src/lib/identity/member-base.adapter.js.map +1 -0
  169. package/src/lib/identity/member-metadata-cache.entity.d.ts +8 -0
  170. package/src/lib/identity/member-metadata-cache.entity.js +39 -0
  171. package/src/lib/identity/member-metadata-cache.entity.js.map +1 -0
  172. package/src/lib/identity/member-profile.object.d.ts +8 -0
  173. package/src/lib/identity/member-profile.object.js +37 -0
  174. package/src/lib/identity/member-profile.object.js.map +1 -0
  175. package/src/lib/identity/member-resolver.interface.d.ts +79 -0
  176. package/src/lib/identity/member-resolver.interface.js +19 -0
  177. package/src/lib/identity/member-resolver.interface.js.map +1 -0
  178. package/src/lib/migrations/0000000000001-enable-postgres-extensions.d.ts +6 -0
  179. package/src/lib/migrations/0000000000001-enable-postgres-extensions.js +17 -0
  180. package/src/lib/migrations/0000000000001-enable-postgres-extensions.js.map +1 -0
  181. package/src/lib/migrations/0000000001000-identity-organization-foundation.d.ts +6 -0
  182. package/src/lib/migrations/{0000000001000-identity-organization-foundation.ts → 0000000001000-identity-organization-foundation.js} +29 -38
  183. package/src/lib/migrations/0000000001000-identity-organization-foundation.js.map +1 -0
  184. package/src/lib/migrations/0000000002000-form-builder-foundation.d.ts +6 -0
  185. package/src/lib/migrations/{0000000002000-form-builder-foundation.ts → 0000000002000-form-builder-foundation.js} +21 -22
  186. package/src/lib/migrations/0000000002000-form-builder-foundation.js.map +1 -0
  187. package/src/lib/migrations/0000000003000-approval-template-foundation.d.ts +6 -0
  188. package/src/lib/migrations/{0000000003000-approval-template-foundation.ts → 0000000003000-approval-template-foundation.js} +21 -24
  189. package/src/lib/migrations/0000000003000-approval-template-foundation.js.map +1 -0
  190. package/src/lib/migrations/0000000004000-workflow-engine-foundation.d.ts +6 -0
  191. package/src/lib/migrations/{0000000004000-workflow-engine-foundation.ts → 0000000004000-workflow-engine-foundation.js} +34 -46
  192. package/src/lib/migrations/0000000004000-workflow-engine-foundation.js.map +1 -0
  193. package/src/lib/migrations/0000000005000-delegation-rules.d.ts +6 -0
  194. package/src/lib/migrations/{0000000005000-delegation-rules.ts → 0000000005000-delegation-rules.js} +19 -20
  195. package/src/lib/migrations/0000000005000-delegation-rules.js.map +1 -0
  196. package/src/lib/migrations/0000000006000-notifications-sla.d.ts +6 -0
  197. package/src/lib/migrations/{0000000006000-notifications-sla.ts → 0000000006000-notifications-sla.js} +23 -26
  198. package/src/lib/migrations/0000000006000-notifications-sla.js.map +1 -0
  199. package/src/lib/migrations/0000000007000-signatures-attachments.d.ts +6 -0
  200. package/src/lib/migrations/{0000000007000-signatures-attachments.ts → 0000000007000-signatures-attachments.js} +27 -28
  201. package/src/lib/migrations/0000000007000-signatures-attachments.js.map +1 -0
  202. package/src/lib/migrations/0000000008000-approval-template-categories.d.ts +6 -0
  203. package/src/lib/migrations/{0000000008000-approval-template-categories.ts → 0000000008000-approval-template-categories.js} +25 -28
  204. package/src/lib/migrations/0000000008000-approval-template-categories.js.map +1 -0
  205. package/src/lib/migrations/0000000009000-task-candidates.d.ts +6 -0
  206. package/src/lib/migrations/{0000000009000-task-candidates.ts → 0000000009000-task-candidates.js} +23 -26
  207. package/src/lib/migrations/0000000009000-task-candidates.js.map +1 -0
  208. package/src/lib/migrations/0000000010000-notification-delivery-state.d.ts +6 -0
  209. package/src/lib/migrations/{0000000010000-notification-delivery-state.ts → 0000000010000-notification-delivery-state.js} +17 -16
  210. package/src/lib/migrations/0000000010000-notification-delivery-state.js.map +1 -0
  211. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.d.ts +6 -0
  212. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js +22 -0
  213. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js.map +1 -0
  214. package/src/lib/migrations/0000000012000-notification-sla-idempotency.d.ts +6 -0
  215. package/src/lib/migrations/0000000012000-notification-sla-idempotency.js +21 -0
  216. package/src/lib/migrations/0000000012000-notification-sla-idempotency.js.map +1 -0
  217. package/src/lib/migrations/0000000013000-workflow-query-indexes.d.ts +6 -0
  218. package/src/lib/migrations/0000000013000-workflow-query-indexes.js +80 -0
  219. package/src/lib/migrations/0000000013000-workflow-query-indexes.js.map +1 -0
  220. package/src/lib/migrations/index.d.ts +16 -0
  221. package/src/lib/migrations/index.js +49 -0
  222. package/src/lib/migrations/index.js.map +1 -0
  223. package/src/lib/notification/dto/notification-preference.input.d.ts +9 -0
  224. package/src/lib/notification/dto/notification-preference.input.js +53 -0
  225. package/src/lib/notification/dto/notification-preference.input.js.map +1 -0
  226. package/src/lib/notification/index.js +11 -0
  227. package/src/lib/notification/index.js.map +1 -0
  228. package/src/lib/notification/notification-delivery-scheduler.service.d.ts +13 -0
  229. package/src/lib/notification/notification-delivery-scheduler.service.js +53 -0
  230. package/src/lib/notification/notification-delivery-scheduler.service.js.map +1 -0
  231. package/src/lib/notification/notification-delivery.service.d.ts +23 -0
  232. package/src/lib/notification/notification-delivery.service.js +245 -0
  233. package/src/lib/notification/notification-delivery.service.js.map +1 -0
  234. package/src/lib/notification/notification-dispatcher.token.d.ts +7 -0
  235. package/src/lib/notification/notification-dispatcher.token.js +5 -0
  236. package/src/lib/notification/notification-dispatcher.token.js.map +1 -0
  237. package/src/lib/notification/notification-options.d.ts +215 -0
  238. package/src/lib/notification/notification-options.js +128 -0
  239. package/src/lib/notification/notification-options.js.map +1 -0
  240. package/src/lib/notification/notification-options.module.d.ts +23 -0
  241. package/src/lib/notification/notification-options.module.js +41 -0
  242. package/src/lib/notification/notification-options.module.js.map +1 -0
  243. package/src/lib/notification/notification-preference.entity.d.ts +10 -0
  244. package/src/lib/notification/notification-preference.entity.js +50 -0
  245. package/src/lib/notification/notification-preference.entity.js.map +1 -0
  246. package/src/lib/notification/notification-sla-scheduler.service.d.ts +13 -0
  247. package/src/lib/notification/notification-sla-scheduler.service.js +51 -0
  248. package/src/lib/notification/notification-sla-scheduler.service.js.map +1 -0
  249. package/src/lib/notification/notification-template.d.ts +13 -0
  250. package/src/lib/notification/notification-template.js +61 -0
  251. package/src/lib/notification/notification-template.js.map +1 -0
  252. package/src/lib/notification/notification.entity.d.ts +23 -0
  253. package/src/lib/notification/notification.entity.js +122 -0
  254. package/src/lib/notification/notification.entity.js.map +1 -0
  255. package/src/lib/notification/notification.enums.d.ts +24 -0
  256. package/src/lib/notification/notification.enums.js +45 -0
  257. package/src/lib/notification/notification.enums.js.map +1 -0
  258. package/src/lib/notification/notification.module.d.ts +2 -0
  259. package/src/lib/notification/notification.module.js +45 -0
  260. package/src/lib/notification/notification.module.js.map +1 -0
  261. package/src/lib/notification/notification.mutations.d.ts +11 -0
  262. package/src/lib/notification/notification.mutations.js +64 -0
  263. package/src/lib/notification/notification.mutations.js.map +1 -0
  264. package/src/lib/notification/notification.queries.d.ts +11 -0
  265. package/src/lib/notification/notification.queries.js +77 -0
  266. package/src/lib/notification/notification.queries.js.map +1 -0
  267. package/src/lib/notification/notification.service.d.ts +81 -0
  268. package/src/lib/notification/notification.service.js +621 -0
  269. package/src/lib/notification/notification.service.js.map +1 -0
  270. package/src/lib/organization/dto/manager-resolution.input.d.ts +18 -0
  271. package/src/lib/organization/dto/manager-resolution.input.js +107 -0
  272. package/src/lib/organization/dto/manager-resolution.input.js.map +1 -0
  273. package/src/lib/organization/dto/membership.input.d.ts +16 -0
  274. package/src/lib/organization/dto/membership.input.js +99 -0
  275. package/src/lib/organization/dto/membership.input.js.map +1 -0
  276. package/src/lib/organization/dto/org-unit.input.d.ts +24 -0
  277. package/src/lib/organization/dto/org-unit.input.js +119 -0
  278. package/src/lib/organization/dto/org-unit.input.js.map +1 -0
  279. package/src/lib/organization/dto/position.input.d.ts +13 -0
  280. package/src/lib/organization/dto/position.input.js +70 -0
  281. package/src/lib/organization/dto/position.input.js.map +1 -0
  282. package/src/lib/organization/index.js +16 -0
  283. package/src/lib/organization/index.js.map +1 -0
  284. package/src/lib/organization/json-metadata.d.ts +1 -0
  285. package/src/lib/organization/json-metadata.js +14 -0
  286. package/src/lib/organization/json-metadata.js.map +1 -0
  287. package/src/lib/organization/manager-resolution.entity.d.ts +11 -0
  288. package/src/lib/organization/manager-resolution.entity.js +55 -0
  289. package/src/lib/organization/manager-resolution.entity.js.map +1 -0
  290. package/src/lib/organization/membership.entity.d.ts +11 -0
  291. package/src/lib/organization/membership.entity.js +60 -0
  292. package/src/lib/organization/membership.entity.js.map +1 -0
  293. package/src/lib/organization/org-unit-tree-commit-result.object.d.ts +4 -0
  294. package/src/lib/organization/org-unit-tree-commit-result.object.js +17 -0
  295. package/src/lib/organization/org-unit-tree-commit-result.object.js.map +1 -0
  296. package/src/lib/organization/org-unit.entity.d.ts +13 -0
  297. package/src/lib/organization/org-unit.entity.js +64 -0
  298. package/src/lib/organization/org-unit.entity.js.map +1 -0
  299. package/src/lib/organization/organization-summary.object.d.ts +6 -0
  300. package/src/lib/organization/organization-summary.object.js +28 -0
  301. package/src/lib/organization/organization-summary.object.js.map +1 -0
  302. package/src/lib/organization/organization.enums.d.ts +11 -0
  303. package/src/lib/organization/organization.enums.js +22 -0
  304. package/src/lib/organization/organization.enums.js.map +1 -0
  305. package/src/lib/organization/organization.module.d.ts +2 -0
  306. package/src/lib/organization/organization.module.js +35 -0
  307. package/src/lib/organization/organization.module.js.map +1 -0
  308. package/src/lib/organization/organization.mutations.d.ts +26 -0
  309. package/src/lib/organization/organization.mutations.js +148 -0
  310. package/src/lib/organization/organization.mutations.js.map +1 -0
  311. package/src/lib/organization/organization.queries.d.ts +23 -0
  312. package/src/lib/organization/organization.queries.js +202 -0
  313. package/src/lib/organization/organization.queries.js.map +1 -0
  314. package/src/lib/organization/organization.service.d.ts +102 -0
  315. package/src/lib/organization/organization.service.js +751 -0
  316. package/src/lib/organization/organization.service.js.map +1 -0
  317. package/src/lib/organization/position.entity.d.ts +9 -0
  318. package/src/lib/organization/position.entity.js +48 -0
  319. package/src/lib/organization/position.entity.js.map +1 -0
  320. package/src/lib/organization/resolved-manager.object.d.ts +4 -0
  321. package/src/lib/organization/resolved-manager.object.js +20 -0
  322. package/src/lib/organization/resolved-manager.object.js.map +1 -0
  323. package/src/lib/signature/index.js +8 -0
  324. package/src/lib/signature/index.js.map +1 -0
  325. package/src/lib/signature/signature-options.d.ts +33 -0
  326. package/src/lib/signature/signature-options.js +51 -0
  327. package/src/lib/signature/signature-options.js.map +1 -0
  328. package/src/lib/signature/signature-verification.object.d.ts +6 -0
  329. package/src/lib/signature/signature-verification.object.js +28 -0
  330. package/src/lib/signature/signature-verification.object.js.map +1 -0
  331. package/src/lib/signature/signature.entity.d.ts +16 -0
  332. package/src/lib/signature/signature.entity.js +88 -0
  333. package/src/lib/signature/signature.entity.js.map +1 -0
  334. package/src/lib/signature/signature.module.d.ts +11 -0
  335. package/src/lib/signature/signature.module.js +60 -0
  336. package/src/lib/signature/signature.module.js.map +1 -0
  337. package/src/lib/signature/signature.queries.d.ts +11 -0
  338. package/src/lib/signature/signature.queries.js +46 -0
  339. package/src/lib/signature/signature.queries.js.map +1 -0
  340. package/src/lib/signature/signature.service.d.ts +25 -0
  341. package/src/lib/signature/signature.service.js +124 -0
  342. package/src/lib/signature/signature.service.js.map +1 -0
  343. package/src/lib/template/approval-template-category.entity.d.ts +9 -0
  344. package/src/lib/template/approval-template-category.entity.js +49 -0
  345. package/src/lib/template/approval-template-category.entity.js.map +1 -0
  346. package/src/lib/template/approval-template-version.entity.d.ts +21 -0
  347. package/src/lib/template/approval-template-version.entity.js +106 -0
  348. package/src/lib/template/approval-template-version.entity.js.map +1 -0
  349. package/src/lib/template/approval-template.entity.d.ts +14 -0
  350. package/src/lib/template/approval-template.entity.js +74 -0
  351. package/src/lib/template/approval-template.entity.js.map +1 -0
  352. package/src/lib/template/dto/approval-template.input.d.ts +36 -0
  353. package/src/lib/template/dto/approval-template.input.js +187 -0
  354. package/src/lib/template/dto/approval-template.input.js.map +1 -0
  355. package/src/lib/template/index.js +12 -0
  356. package/src/lib/template/index.js.map +1 -0
  357. package/src/lib/template/template.enums.d.ts +14 -0
  358. package/src/lib/template/template.enums.js +31 -0
  359. package/src/lib/template/template.enums.js.map +1 -0
  360. package/src/lib/template/template.module.d.ts +2 -0
  361. package/src/lib/template/template.module.js +33 -0
  362. package/src/lib/template/template.module.js.map +1 -0
  363. package/src/lib/template/template.mutations.d.ts +20 -0
  364. package/src/lib/template/template.mutations.js +139 -0
  365. package/src/lib/template/template.mutations.js.map +1 -0
  366. package/src/lib/template/template.queries.d.ts +16 -0
  367. package/src/lib/template/template.queries.js +133 -0
  368. package/src/lib/template/template.queries.js.map +1 -0
  369. package/src/lib/template/template.service.d.ts +64 -0
  370. package/src/lib/template/template.service.js +521 -0
  371. package/src/lib/template/template.service.js.map +1 -0
  372. package/src/lib/template/workflow-definition.validator.d.ts +9 -0
  373. package/src/lib/template/workflow-definition.validator.js +362 -0
  374. package/src/lib/template/workflow-definition.validator.js.map +1 -0
  375. package/src/lib/testing/cel-js.jest.d.ts +15 -0
  376. package/src/lib/testing/cel-js.jest.js +27 -0
  377. package/src/lib/testing/cel-js.jest.js.map +1 -0
  378. package/src/lib/workflow-engine/activity-log.entity.d.ts +12 -0
  379. package/src/lib/workflow-engine/activity-log.entity.js +62 -0
  380. package/src/lib/workflow-engine/activity-log.entity.js.map +1 -0
  381. package/src/lib/workflow-engine/approval-instance-page-info.object.d.ts +8 -0
  382. package/src/lib/workflow-engine/approval-instance-page-info.object.js +36 -0
  383. package/src/lib/workflow-engine/approval-instance-page-info.object.js.map +1 -0
  384. package/src/lib/workflow-engine/approval-instance.entity.d.ts +22 -0
  385. package/src/lib/workflow-engine/approval-instance.entity.js +113 -0
  386. package/src/lib/workflow-engine/approval-instance.entity.js.map +1 -0
  387. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.d.ts +5 -0
  388. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js +29 -0
  389. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js.map +1 -0
  390. package/src/lib/workflow-engine/dto/decide-task.input.d.ts +9 -0
  391. package/src/lib/workflow-engine/dto/decide-task.input.js +47 -0
  392. package/src/lib/workflow-engine/dto/decide-task.input.js.map +1 -0
  393. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.d.ts +6 -0
  394. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js +34 -0
  395. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js.map +1 -0
  396. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.d.ts +6 -0
  397. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js +34 -0
  398. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js.map +1 -0
  399. package/src/lib/workflow-engine/dto/submit-approval-instance.input.d.ts +7 -0
  400. package/src/lib/workflow-engine/dto/submit-approval-instance.input.js +40 -0
  401. package/src/lib/workflow-engine/dto/submit-approval-instance.input.js.map +1 -0
  402. package/src/lib/workflow-engine/index.js +24 -0
  403. package/src/lib/workflow-engine/index.js.map +1 -0
  404. package/src/lib/workflow-engine/task-candidate.entity.d.ts +14 -0
  405. package/src/lib/workflow-engine/task-candidate.entity.js +72 -0
  406. package/src/lib/workflow-engine/task-candidate.entity.js.map +1 -0
  407. package/src/lib/workflow-engine/task-decision.entity.d.ts +12 -0
  408. package/src/lib/workflow-engine/task-decision.entity.js +60 -0
  409. package/src/lib/workflow-engine/task-decision.entity.js.map +1 -0
  410. package/src/lib/workflow-engine/task.entity.d.ts +20 -0
  411. package/src/lib/workflow-engine/task.entity.js +109 -0
  412. package/src/lib/workflow-engine/task.entity.js.map +1 -0
  413. package/src/lib/workflow-engine/workflow-condition-evaluator.d.ts +5 -0
  414. package/src/lib/workflow-engine/workflow-condition-evaluator.js +107 -0
  415. package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -0
  416. package/src/lib/workflow-engine/workflow-dashboard-summary.object.d.ts +8 -0
  417. package/src/lib/workflow-engine/workflow-dashboard-summary.object.js +36 -0
  418. package/src/lib/workflow-engine/workflow-dashboard-summary.object.js.map +1 -0
  419. package/src/lib/workflow-engine/workflow-dry-run.object.d.ts +21 -0
  420. package/src/lib/workflow-engine/workflow-dry-run.object.js +86 -0
  421. package/src/lib/workflow-engine/workflow-dry-run.object.js.map +1 -0
  422. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +58 -0
  423. package/src/lib/workflow-engine/workflow-engine.enums.js +95 -0
  424. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -0
  425. package/src/lib/workflow-engine/workflow-engine.module.d.ts +9 -0
  426. package/src/lib/workflow-engine/workflow-engine.module.js +76 -0
  427. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -0
  428. package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +19 -0
  429. package/src/lib/workflow-engine/workflow-engine.mutations.js +106 -0
  430. package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -0
  431. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +29 -0
  432. package/src/lib/workflow-engine/workflow-engine.queries.js +225 -0
  433. package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -0
  434. package/src/lib/workflow-engine/workflow-engine.service.d.ts +145 -0
  435. package/src/lib/workflow-engine/workflow-engine.service.js +2785 -0
  436. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -0
  437. package/src/lib/workflow-engine/{workflow-engine.tokens.ts → workflow-engine.tokens.d.ts} +3 -9
  438. package/src/lib/workflow-engine/workflow-engine.tokens.js +5 -0
  439. package/src/lib/workflow-engine/workflow-engine.tokens.js.map +1 -0
  440. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.d.ts +18 -0
  441. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +39 -0
  442. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -0
  443. package/src/lib/workflow-engine/workflow-token.entity.d.ts +10 -0
  444. package/src/lib/workflow-engine/workflow-token.entity.js +50 -0
  445. package/src/lib/workflow-engine/workflow-token.entity.js.map +1 -0
  446. package/eslint.config.mjs +0 -20
  447. package/jest.config.cts +0 -15
  448. package/project.json +0 -58
  449. package/src/lib/attachment/attachment-options.spec.ts +0 -45
  450. package/src/lib/attachment/attachment-options.ts +0 -130
  451. package/src/lib/attachment/attachment-storage.provider.spec.ts +0 -79
  452. package/src/lib/attachment/attachment-storage.provider.ts +0 -60
  453. package/src/lib/attachment/attachment.controller.ts +0 -39
  454. package/src/lib/attachment/attachment.entity.ts +0 -59
  455. package/src/lib/attachment/attachment.module.ts +0 -149
  456. package/src/lib/attachment/attachment.mutations.ts +0 -22
  457. package/src/lib/attachment/attachment.queries.ts +0 -65
  458. package/src/lib/attachment/attachment.service.spec.ts +0 -319
  459. package/src/lib/attachment/attachment.service.ts +0 -477
  460. package/src/lib/attachment/dto/upload-attachment.input.ts +0 -60
  461. package/src/lib/bpm/bpm-root.module.spec.ts +0 -138
  462. package/src/lib/bpm/bpm-root.module.ts +0 -320
  463. package/src/lib/bpm-auth/bpm-auth-context.extractor.ts +0 -106
  464. package/src/lib/bpm-auth/bpm-auth-context.ts +0 -40
  465. package/src/lib/bpm-auth/bpm-auth.authorization.ts +0 -80
  466. package/src/lib/bpm-auth/bpm-auth.decorators.ts +0 -19
  467. package/src/lib/bpm-auth/bpm-auth.guard.ts +0 -31
  468. package/src/lib/bpm-auth/bpm-auth.module.spec.ts +0 -106
  469. package/src/lib/bpm-auth/bpm-auth.module.ts +0 -69
  470. package/src/lib/bpm-auth/bpm-auth.options.ts +0 -21
  471. package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.ts +0 -55
  472. package/src/lib/common/filters/all-exceptions.filter.ts +0 -138
  473. package/src/lib/common/iso-duration.ts +0 -31
  474. package/src/lib/condition/condition.module.ts +0 -8
  475. package/src/lib/condition/condition.service.spec.ts +0 -50
  476. package/src/lib/condition/condition.service.ts +0 -141
  477. package/src/lib/database/data-source.ts +0 -7
  478. package/src/lib/database/migration-runner.ts +0 -24
  479. package/src/lib/database/typeorm.config.ts +0 -217
  480. package/src/lib/delegation/delegation-rule.entity.ts +0 -84
  481. package/src/lib/delegation/delegation.enums.ts +0 -21
  482. package/src/lib/delegation/delegation.module.ts +0 -14
  483. package/src/lib/delegation/delegation.mutations.ts +0 -92
  484. package/src/lib/delegation/delegation.queries.ts +0 -111
  485. package/src/lib/delegation/delegation.service.spec.ts +0 -295
  486. package/src/lib/delegation/delegation.service.ts +0 -482
  487. package/src/lib/delegation/dto/delegation-rule.input.ts +0 -125
  488. package/src/lib/form/dto/form-definition.input.ts +0 -73
  489. package/src/lib/form/form-definition-version.entity.ts +0 -66
  490. package/src/lib/form/form-definition.entity.ts +0 -54
  491. package/src/lib/form/form-schema-lint.object.ts +0 -10
  492. package/src/lib/form/form-schema.validator.spec.ts +0 -180
  493. package/src/lib/form/form-schema.validator.ts +0 -538
  494. package/src/lib/form/form.enums.ts +0 -20
  495. package/src/lib/form/form.module.ts +0 -19
  496. package/src/lib/form/form.mutations.ts +0 -68
  497. package/src/lib/form/form.queries.ts +0 -72
  498. package/src/lib/form/form.service.spec.ts +0 -144
  499. package/src/lib/form/form.service.ts +0 -490
  500. package/src/lib/identity/identity-options.ts +0 -41
  501. package/src/lib/identity/identity.module.ts +0 -96
  502. package/src/lib/identity/identity.queries.ts +0 -61
  503. package/src/lib/identity/identity.service.spec.ts +0 -105
  504. package/src/lib/identity/identity.service.ts +0 -200
  505. package/src/lib/identity/member-base.adapter.spec.ts +0 -77
  506. package/src/lib/identity/member-base.adapter.ts +0 -244
  507. package/src/lib/identity/member-metadata-cache.entity.ts +0 -27
  508. package/src/lib/identity/member-profile.object.ts +0 -28
  509. package/src/lib/identity/member-resolver.interface.ts +0 -47
  510. package/src/lib/migrations/0000000000001-enable-postgres-extensions.ts +0 -14
  511. package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.ts +0 -19
  512. package/src/lib/migrations/0000000012000-notification-sla-idempotency.ts +0 -22
  513. package/src/lib/migrations/0000000013000-workflow-query-indexes.ts +0 -113
  514. package/src/lib/migrations/index.ts +0 -47
  515. package/src/lib/notification/dto/notification-preference.input.ts +0 -46
  516. package/src/lib/notification/notification-delivery-scheduler.service.ts +0 -69
  517. package/src/lib/notification/notification-delivery.service.spec.ts +0 -315
  518. package/src/lib/notification/notification-delivery.service.ts +0 -339
  519. package/src/lib/notification/notification-dispatcher.token.ts +0 -13
  520. package/src/lib/notification/notification-options.module.ts +0 -67
  521. package/src/lib/notification/notification-options.spec.ts +0 -93
  522. package/src/lib/notification/notification-options.ts +0 -452
  523. package/src/lib/notification/notification-preference.entity.ts +0 -35
  524. package/src/lib/notification/notification-sla-scheduler.service.ts +0 -66
  525. package/src/lib/notification/notification-template.spec.ts +0 -33
  526. package/src/lib/notification/notification-template.ts +0 -97
  527. package/src/lib/notification/notification.entity.ts +0 -102
  528. package/src/lib/notification/notification.enums.ts +0 -45
  529. package/src/lib/notification/notification.module.ts +0 -37
  530. package/src/lib/notification/notification.mutations.ts +0 -47
  531. package/src/lib/notification/notification.queries.ts +0 -64
  532. package/src/lib/notification/notification.service.spec.ts +0 -412
  533. package/src/lib/notification/notification.service.ts +0 -974
  534. package/src/lib/organization/dto/manager-resolution.input.ts +0 -91
  535. package/src/lib/organization/dto/membership.input.ts +0 -77
  536. package/src/lib/organization/dto/org-unit.input.ts +0 -96
  537. package/src/lib/organization/dto/position.input.ts +0 -50
  538. package/src/lib/organization/json-metadata.ts +0 -15
  539. package/src/lib/organization/manager-resolution.entity.ts +0 -44
  540. package/src/lib/organization/membership.entity.ts +0 -50
  541. package/src/lib/organization/org-unit-tree-commit-result.object.ts +0 -8
  542. package/src/lib/organization/org-unit.entity.ts +0 -53
  543. package/src/lib/organization/organization-summary.object.ts +0 -16
  544. package/src/lib/organization/organization.enums.ts +0 -19
  545. package/src/lib/organization/organization.module.ts +0 -27
  546. package/src/lib/organization/organization.mutations.ts +0 -115
  547. package/src/lib/organization/organization.queries.ts +0 -188
  548. package/src/lib/organization/organization.service.spec.ts +0 -760
  549. package/src/lib/organization/organization.service.ts +0 -1216
  550. package/src/lib/organization/position.entity.ts +0 -39
  551. package/src/lib/organization/resolved-manager.object.ts +0 -10
  552. package/src/lib/signature/signature-options.spec.ts +0 -60
  553. package/src/lib/signature/signature-options.ts +0 -120
  554. package/src/lib/signature/signature-verification.object.ts +0 -16
  555. package/src/lib/signature/signature.entity.ts +0 -62
  556. package/src/lib/signature/signature.module.ts +0 -80
  557. package/src/lib/signature/signature.queries.ts +0 -37
  558. package/src/lib/signature/signature.service.spec.ts +0 -134
  559. package/src/lib/signature/signature.service.ts +0 -205
  560. package/src/lib/template/approval-template-category.entity.ts +0 -40
  561. package/src/lib/template/approval-template-version.entity.ts +0 -82
  562. package/src/lib/template/approval-template.entity.ts +0 -64
  563. package/src/lib/template/dto/approval-template.input.ts +0 -147
  564. package/src/lib/template/template.enums.ts +0 -30
  565. package/src/lib/template/template.module.ts +0 -25
  566. package/src/lib/template/template.mutations.ts +0 -106
  567. package/src/lib/template/template.queries.ts +0 -119
  568. package/src/lib/template/template.service.spec.ts +0 -476
  569. package/src/lib/template/template.service.ts +0 -820
  570. package/src/lib/template/workflow-definition.validator.spec.ts +0 -595
  571. package/src/lib/template/workflow-definition.validator.ts +0 -539
  572. package/src/lib/testing/cel-js.jest.ts +0 -46
  573. package/src/lib/workflow-engine/activity-log.entity.ts +0 -48
  574. package/src/lib/workflow-engine/approval-instance-page-info.object.ts +0 -22
  575. package/src/lib/workflow-engine/approval-instance.entity.ts +0 -86
  576. package/src/lib/workflow-engine/dto/cancel-approval-instance.input.ts +0 -18
  577. package/src/lib/workflow-engine/dto/decide-task.input.ts +0 -33
  578. package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.ts +0 -22
  579. package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.ts +0 -22
  580. package/src/lib/workflow-engine/dto/submit-approval-instance.input.ts +0 -27
  581. package/src/lib/workflow-engine/task-candidate.entity.ts +0 -56
  582. package/src/lib/workflow-engine/task-decision.entity.ts +0 -43
  583. package/src/lib/workflow-engine/task.entity.ts +0 -85
  584. package/src/lib/workflow-engine/workflow-condition-evaluator.ts +0 -168
  585. package/src/lib/workflow-engine/workflow-dashboard-summary.object.ts +0 -22
  586. package/src/lib/workflow-engine/workflow-dry-run.object.ts +0 -58
  587. package/src/lib/workflow-engine/workflow-engine.enums.ts +0 -99
  588. package/src/lib/workflow-engine/workflow-engine.module.ts +0 -75
  589. package/src/lib/workflow-engine/workflow-engine.mutations.ts +0 -85
  590. package/src/lib/workflow-engine/workflow-engine.queries.ts +0 -229
  591. package/src/lib/workflow-engine/workflow-engine.service.spec.ts +0 -2689
  592. package/src/lib/workflow-engine/workflow-engine.service.ts +0 -4707
  593. package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.ts +0 -69
  594. package/src/lib/workflow-engine/workflow-token.entity.ts +0 -40
  595. package/tsconfig.json +0 -24
  596. package/tsconfig.lib.json +0 -16
  597. package/tsconfig.spec.json +0 -16
  598. /package/src/{index.ts → index.d.ts} +0 -0
  599. /package/src/lib/attachment/{index.ts → index.d.ts} +0 -0
  600. /package/src/lib/bpm/{index.ts → index.d.ts} +0 -0
  601. /package/src/lib/bpm-auth/{index.ts → index.d.ts} +0 -0
  602. /package/src/lib/common/{index.ts → index.d.ts} +0 -0
  603. /package/src/lib/condition/{index.ts → index.d.ts} +0 -0
  604. /package/src/lib/database/{index.ts → index.d.ts} +0 -0
  605. /package/src/lib/delegation/{index.ts → index.d.ts} +0 -0
  606. /package/src/lib/form/{index.ts → index.d.ts} +0 -0
  607. /package/src/lib/identity/{index.ts → index.d.ts} +0 -0
  608. /package/src/lib/notification/{index.ts → index.d.ts} +0 -0
  609. /package/src/lib/organization/{index.ts → index.d.ts} +0 -0
  610. /package/src/lib/signature/{index.ts → index.d.ts} +0 -0
  611. /package/src/lib/template/{index.ts → index.d.ts} +0 -0
  612. /package/src/lib/workflow-engine/{index.ts → index.d.ts} +0 -0
@@ -1,4707 +0,0 @@
1
- import {
2
- FormDefinitionSchema,
3
- FormFieldDefinition,
4
- NumberFieldDefinition,
5
- } from '@rytass/bpm-core-shared/form';
6
- import {
7
- WorkflowDefinition,
8
- WorkflowEdge,
9
- DecisionPolicy,
10
- WorkflowNode,
11
- UserTaskNode,
12
- ServiceTaskNode,
13
- ApproverResolver,
14
- ApproverResolverFallback,
15
- ReturnResubmitStrategy,
16
- } from '@rytass/bpm-core-shared/workflow';
17
- import {
18
- BadRequestException,
19
- ConflictException,
20
- Inject,
21
- Injectable,
22
- NotFoundException,
23
- Optional,
24
- } from '@nestjs/common';
25
- import { InjectRepository } from '@nestjs/typeorm';
26
- import {
27
- EntityManager,
28
- In,
29
- IsNull,
30
- Repository,
31
- SelectQueryBuilder,
32
- } from 'typeorm';
33
- import { AttachmentService } from '../attachment/attachment.service';
34
- import {
35
- DelegationService,
36
- DelegationStep,
37
- } from '../delegation/delegation.service';
38
- import { FormDefinitionVersionEntity } from '../form/form-definition-version.entity';
39
- import { FormDefinitionVersionStatusEnum } from '../form/form.enums';
40
- import {
41
- calculateTaskSlaDueAt,
42
- NotificationService,
43
- } from '../notification/notification.service';
44
- import { NotificationEntity } from '../notification/notification.entity';
45
- import { SignatureService } from '../signature/signature.service';
46
- import { ConditionService } from '../condition/condition.service';
47
- import { BPMAuthContext } from '../bpm-auth';
48
- import { ManagerResolutionEntity } from '../organization/manager-resolution.entity';
49
- import { MembershipEntity } from '../organization/membership.entity';
50
- import { OrgUnitEntity } from '../organization/org-unit.entity';
51
- import { ManagerResolutionScopeTypeEnum } from '../organization/organization.enums';
52
- import { ApprovalTemplateVersionEntity } from '../template/approval-template-version.entity';
53
- import { ApprovalTemplateEntity } from '../template/approval-template.entity';
54
- import { CancelApprovalInstanceInput } from './dto/cancel-approval-instance.input';
55
- import { ApprovalTemplateVersionStatusEnum } from '../template/template.enums';
56
- import { DecideTaskInput } from './dto/decide-task.input';
57
- import { DryRunApprovalWorkflowInput } from './dto/dry-run-approval-workflow.input';
58
- import { ResubmitApprovalInstanceInput } from './dto/resubmit-approval-instance.input';
59
- import { SubmitApprovalInstanceInput } from './dto/submit-approval-instance.input';
60
- import { ActivityLogEntity } from './activity-log.entity';
61
- import { ApprovalInstanceEntity } from './approval-instance.entity';
62
- import { TaskDecisionEntity } from './task-decision.entity';
63
- import { TaskCandidateEntity } from './task-candidate.entity';
64
- import { TaskEntity } from './task.entity';
65
- import {
66
- ActivityLogEventTypeEnum,
67
- ApprovalInstanceListViewEnum,
68
- ApprovalInstanceStateEnum,
69
- TaskAssignmentTypeEnum,
70
- TaskCandidateStatusEnum,
71
- TaskDecisionActionEnum,
72
- TaskStatusEnum,
73
- WorkflowTokenStatusEnum,
74
- } from './workflow-engine.enums';
75
- import { evaluateWorkflowEdgeCondition } from './workflow-condition-evaluator';
76
- import {
77
- WorkflowDryRunResultObject,
78
- WorkflowDryRunStepObject,
79
- } from './workflow-dry-run.object';
80
- import {
81
- BPM_WORKFLOW_SERVICE_TASK_DISPATCHER,
82
- BPMWorkflowServiceTaskDispatcher,
83
- BPMWorkflowWebhookDispatchResult,
84
- DefaultWorkflowServiceTaskDispatcher,
85
- } from './workflow-service-task-dispatcher.token';
86
- import { WorkflowTokenEntity } from './workflow-token.entity';
87
-
88
- const MAX_PROCESSING_STEPS = 500;
89
- const DEFAULT_APPROVAL_HISTORY_TASK_LIMIT = 50;
90
- const SYSTEM_DRY_RUN_INSTANCE_ID = 'dry-run-instance';
91
-
92
- interface DryRunSimulationInput {
93
- readonly formData: Readonly<Record<string, unknown>>;
94
- readonly initiatorMemberId: string;
95
- readonly initiatorMetadataSnapshot: Readonly<Record<string, unknown>>;
96
- readonly workflowDefinition: WorkflowDefinition;
97
- }
98
-
99
- interface DryRunPathInput extends DryRunSimulationInput {
100
- readonly depth: number;
101
- readonly incomingEdge: WorkflowEdge | null;
102
- readonly lastDecision: Readonly<Record<string, unknown>> | null;
103
- readonly nodeId: string;
104
- readonly steps: readonly WorkflowDryRunStepObject[];
105
- readonly visitedNodeIds: ReadonlySet<string>;
106
- }
107
-
108
- interface ReturnedInstanceContext {
109
- readonly resubmitStrategy: ReturnResubmitStrategy;
110
- readonly returnedFromNodeId: string;
111
- readonly returnToNodeId: string;
112
- }
113
-
114
- interface ResolvedApproverCandidate {
115
- readonly memberId: string;
116
- readonly sourceType: ApproverResolver['type'];
117
- }
118
-
119
- interface RuntimeTaskCandidate {
120
- readonly delegationChain: readonly DelegationStep[];
121
- readonly memberId: string;
122
- readonly originalMemberId: string;
123
- readonly sourceType: ApproverResolver['type'];
124
- }
125
-
126
- interface ListApprovalInstancesOptions {
127
- readonly page?: number;
128
- readonly pageSize?: number;
129
- readonly searchText?: string;
130
- readonly state?: readonly ApprovalInstanceStateEnum[];
131
- readonly templateId?: string;
132
- readonly view?: ApprovalInstanceListViewEnum;
133
- }
134
-
135
- interface WorkflowDashboardSummaryOptions {
136
- readonly from?: Date;
137
- readonly to?: Date;
138
- }
139
-
140
- type WorkflowReadScope = BPMAuthContext;
141
- type FormConditionOperator =
142
- | 'equals'
143
- | 'greaterThan'
144
- | 'greaterThanOrEqual'
145
- | 'lessThan'
146
- | 'lessThanOrEqual'
147
- | 'notEquals';
148
-
149
- const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
150
- 'bpm:*',
151
- 'bpm.workflow.read_all',
152
- 'bpm:workflow:read_all',
153
- 'workflow.read_all',
154
- ]);
155
-
156
- @Injectable()
157
- export class WorkflowEngineService {
158
- constructor(
159
- @InjectRepository(ApprovalInstanceEntity)
160
- private readonly approvalInstanceRepository: Repository<ApprovalInstanceEntity>,
161
- @InjectRepository(WorkflowTokenEntity)
162
- private readonly workflowTokenRepository: Repository<WorkflowTokenEntity>,
163
- @InjectRepository(TaskEntity)
164
- private readonly taskRepository: Repository<TaskEntity>,
165
- @InjectRepository(TaskCandidateEntity)
166
- private readonly taskCandidateRepository: Repository<TaskCandidateEntity>,
167
- @InjectRepository(TaskDecisionEntity)
168
- private readonly taskDecisionRepository: Repository<TaskDecisionEntity>,
169
- @InjectRepository(NotificationEntity)
170
- private readonly notificationRepository: Repository<NotificationEntity>,
171
- @InjectRepository(ActivityLogEntity)
172
- private readonly activityLogRepository: Repository<ActivityLogEntity>,
173
- @InjectRepository(ApprovalTemplateEntity)
174
- private readonly approvalTemplateRepository: Repository<ApprovalTemplateEntity>,
175
- @InjectRepository(ApprovalTemplateVersionEntity)
176
- private readonly approvalTemplateVersionRepository: Repository<ApprovalTemplateVersionEntity>,
177
- @InjectRepository(FormDefinitionVersionEntity)
178
- private readonly formDefinitionVersionRepository: Repository<FormDefinitionVersionEntity>,
179
- private readonly attachmentService: AttachmentService,
180
- private readonly conditionService: ConditionService,
181
- private readonly delegationService: DelegationService,
182
- private readonly notificationService: NotificationService,
183
- private readonly signatureService: SignatureService,
184
- @Optional()
185
- @Inject(BPM_WORKFLOW_SERVICE_TASK_DISPATCHER)
186
- private readonly serviceTaskDispatcher: BPMWorkflowServiceTaskDispatcher = new DefaultWorkflowServiceTaskDispatcher(),
187
- ) {}
188
-
189
- async submitApprovalInstance(
190
- input: SubmitApprovalInstanceInput,
191
- ): Promise<ApprovalInstanceEntity> {
192
- const formData = parseJsonObject(input.formDataJson, 'formDataJson');
193
- const template = await this.getTemplateOrThrow(input.templateId);
194
-
195
- if (!template.currentVersionId) {
196
- throw new ConflictException(
197
- 'Approval template does not have a published version',
198
- );
199
- }
200
-
201
- const templateVersion = await this.getPublishedTemplateVersionOrThrow(
202
- template.currentVersionId,
203
- );
204
- const formDefinitionVersion = await this.getPublishedFormVersionOrThrow(
205
- templateVersion.formDefinitionVersionId,
206
- );
207
- validateSubmittedFormData(formDefinitionVersion.schema, formData);
208
- const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
209
- ? parseJsonObject(
210
- input.initiatorMetadataSnapshotJson,
211
- 'initiatorMetadataSnapshotJson',
212
- )
213
- : await this.readDefaultInitiatorMetadataSnapshot(
214
- input.initiatorMemberId,
215
- );
216
-
217
- if (
218
- !this.conditionService.evaluateBoolean(
219
- templateVersion.initiatorPolicyCel,
220
- buildInitiatorPolicyContext(initiatorMetadataSnapshot),
221
- 'initiatorPolicyCel',
222
- )
223
- ) {
224
- throw new ConflictException(
225
- 'Current member is not allowed to start this approval template',
226
- );
227
- }
228
-
229
- const startedAt = new Date();
230
- const startNode = templateVersion.workflowDefinition.nodes.find(
231
- (node) => node.type === 'startEvent',
232
- );
233
-
234
- if (!startNode) {
235
- throw new BadRequestException(
236
- 'Published workflow does not include a startEvent',
237
- );
238
- }
239
-
240
- return this.approvalInstanceRepository.manager.transaction(
241
- async (manager): Promise<ApprovalInstanceEntity> => {
242
- const instanceRepository = manager.getRepository(
243
- ApprovalInstanceEntity,
244
- );
245
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
246
- const activityRepository = manager.getRepository(ActivityLogEntity);
247
- const instance = await instanceRepository.save(
248
- instanceRepository.create({
249
- completedAt: null,
250
- formData,
251
- formDefinitionSnapshot: {
252
- formDefinitionVersionId: formDefinitionVersion.id,
253
- schema: formDefinitionVersion.schema,
254
- uiSchema: formDefinitionVersion.uiSchema,
255
- version: formDefinitionVersion.version,
256
- },
257
- initiatorMemberId: input.initiatorMemberId,
258
- initiatorMetadataSnapshot,
259
- startedAt,
260
- state: ApprovalInstanceStateEnum.RUNNING,
261
- templateId: template.id,
262
- templateVersionId: templateVersion.id,
263
- title: input.title?.trim() || template.name,
264
- workflowSnapshot: templateVersion.workflowDefinition,
265
- }),
266
- );
267
- await this.attachmentService.bindFormDataAttachmentsToInstance(
268
- manager,
269
- {
270
- formData,
271
- instanceId: instance.id,
272
- },
273
- );
274
- const token = await tokenRepository.save(
275
- tokenRepository.create({
276
- consumedAt: null,
277
- currentNodeId: startNode.id,
278
- instanceId: instance.id,
279
- parentTokenId: null,
280
- status: WorkflowTokenStatusEnum.ACTIVE,
281
- }),
282
- );
283
-
284
- await activityRepository.save([
285
- activityRepository.create({
286
- actorMemberId: input.initiatorMemberId,
287
- eventType: ActivityLogEventTypeEnum.INSTANCE_STARTED,
288
- instanceId: instance.id,
289
- nodeId: startNode.id,
290
- payload: {
291
- templateId: template.id,
292
- templateVersionId: templateVersion.id,
293
- },
294
- taskId: null,
295
- }),
296
- activityRepository.create({
297
- actorMemberId: null,
298
- eventType: ActivityLogEventTypeEnum.TOKEN_CREATED,
299
- instanceId: instance.id,
300
- nodeId: startNode.id,
301
- payload: {
302
- tokenId: token.id,
303
- },
304
- taskId: null,
305
- }),
306
- ]);
307
- await this.processRunningInstance(manager, instance);
308
-
309
- return instance;
310
- },
311
- );
312
- }
313
-
314
- async processInstance(instanceId: string): Promise<void> {
315
- await this.approvalInstanceRepository.manager.transaction(
316
- async (manager): Promise<void> => {
317
- await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
318
- instanceId,
319
- ]);
320
-
321
- const instance = await manager
322
- .getRepository(ApprovalInstanceEntity)
323
- .findOne({ where: { id: instanceId } });
324
-
325
- if (!instance) {
326
- throw new NotFoundException(
327
- `Approval instance ${instanceId} was not found`,
328
- );
329
- }
330
-
331
- await manager.getRepository(ActivityLogEntity).save(
332
- manager.getRepository(ActivityLogEntity).create({
333
- actorMemberId: null,
334
- eventType: ActivityLogEventTypeEnum.ENGINE_PROCESS_REQUESTED,
335
- instanceId,
336
- nodeId: null,
337
- payload: {
338
- state: instance.state,
339
- },
340
- taskId: null,
341
- }),
342
- );
343
-
344
- if (instance.state !== ApprovalInstanceStateEnum.RUNNING) {
345
- return;
346
- }
347
-
348
- await this.processRunningInstance(manager, instance);
349
- },
350
- );
351
- }
352
-
353
- async decideTask(input: DecideTaskInput): Promise<TaskDecisionEntity> {
354
- return this.approvalInstanceRepository.manager.transaction(
355
- async (manager): Promise<TaskDecisionEntity> => {
356
- const taskRepository = manager.getRepository(TaskEntity);
357
- const taskCandidateRepository =
358
- manager.getRepository(TaskCandidateEntity);
359
- const taskDecisionRepository =
360
- manager.getRepository(TaskDecisionEntity);
361
- const activityRepository = manager.getRepository(ActivityLogEntity);
362
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
363
- const instanceRepository = manager.getRepository(
364
- ApprovalInstanceEntity,
365
- );
366
- const task = await taskRepository.findOne({
367
- where: { id: input.taskId },
368
- });
369
-
370
- if (!task) {
371
- throw new NotFoundException(`Task ${input.taskId} was not found`);
372
- }
373
-
374
- await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
375
- task.instanceId,
376
- ]);
377
-
378
- if (
379
- task.status !== TaskStatusEnum.PENDING &&
380
- task.status !== TaskStatusEnum.IN_PROGRESS
381
- ) {
382
- throw new ConflictException(`Task ${task.id} is not pending`);
383
- }
384
-
385
- const taskCandidates = await taskCandidateRepository.find({
386
- where: { taskId: task.id },
387
- });
388
- const actorCandidate = taskCandidates.find(
389
- (candidate) => candidate.memberId === input.decidedByMemberId,
390
- );
391
- const isDirectAssignee =
392
- task.assigneeMemberId === input.decidedByMemberId;
393
-
394
- if (!isDirectAssignee && !actorCandidate) {
395
- throw new ConflictException(
396
- `Task ${task.id} is assigned to another member`,
397
- );
398
- }
399
-
400
- if (
401
- actorCandidate &&
402
- actorCandidate.status !== TaskCandidateStatusEnum.PENDING &&
403
- actorCandidate.status !== TaskCandidateStatusEnum.CLAIMED
404
- ) {
405
- throw new ConflictException(
406
- `Task ${task.id} was already decided by this member`,
407
- );
408
- }
409
-
410
- if (
411
- input.action !== TaskDecisionActionEnum.APPROVED &&
412
- input.action !== TaskDecisionActionEnum.REJECTED &&
413
- input.action !== TaskDecisionActionEnum.RETURNED &&
414
- input.action !== TaskDecisionActionEnum.TRANSFERRED
415
- ) {
416
- throw new ConflictException(
417
- `Task decision action ${input.action} is not supported yet`,
418
- );
419
- }
420
- const decisionComment = input.comment?.trim() || null;
421
-
422
- if (
423
- input.action === TaskDecisionActionEnum.REJECTED &&
424
- !decisionComment
425
- ) {
426
- throw new BadRequestException('Reject decision comment is required');
427
- }
428
- const transferToMemberId = input.transferToMemberId?.trim() || null;
429
-
430
- if (
431
- input.action === TaskDecisionActionEnum.TRANSFERRED &&
432
- !transferToMemberId
433
- ) {
434
- throw new BadRequestException('Transfer target member is required');
435
- }
436
-
437
- if (
438
- input.action === TaskDecisionActionEnum.TRANSFERRED &&
439
- transferToMemberId === input.decidedByMemberId
440
- ) {
441
- throw new ConflictException(
442
- 'Transfer target must be a different member',
443
- );
444
- }
445
-
446
- const instance = await instanceRepository.findOne({
447
- where: { id: task.instanceId },
448
- });
449
-
450
- if (!instance) {
451
- throw new NotFoundException(
452
- `Approval instance ${task.instanceId} was not found`,
453
- );
454
- }
455
-
456
- if (instance.state !== ApprovalInstanceStateEnum.RUNNING) {
457
- throw new ConflictException(
458
- `Approval instance ${instance.id} is not running`,
459
- );
460
- }
461
-
462
- const returnToNodeId =
463
- input.action === TaskDecisionActionEnum.RETURNED
464
- ? readReturnTargetNodeId(
465
- instance.workflowSnapshot,
466
- task.nodeId,
467
- input.returnToNodeId,
468
- )
469
- : null;
470
- const decidedAt = new Date();
471
- const claimedTask = await taskRepository.save({
472
- ...task,
473
- assigneeMemberId: task.assigneeMemberId ?? input.decidedByMemberId,
474
- openedAt: task.openedAt ?? decidedAt,
475
- originalAssigneeMemberId:
476
- task.originalAssigneeMemberId ??
477
- actorCandidate?.originalMemberId ??
478
- input.decidedByMemberId,
479
- status: TaskStatusEnum.IN_PROGRESS,
480
- });
481
- const claimedCandidate = actorCandidate
482
- ? await taskCandidateRepository.save({
483
- ...actorCandidate,
484
- claimedAt: actorCandidate.claimedAt ?? decidedAt,
485
- status: TaskCandidateStatusEnum.CLAIMED,
486
- })
487
- : null;
488
- const signature = await this.signatureService.signTaskDecision(
489
- manager,
490
- {
491
- action: input.action,
492
- comment: decisionComment,
493
- decidedAt,
494
- instance,
495
- returnToNodeId,
496
- signerMemberId: input.decidedByMemberId,
497
- task: claimedTask,
498
- transferToMemberId,
499
- },
500
- );
501
- const decision = await taskDecisionRepository.save(
502
- taskDecisionRepository.create({
503
- action: input.action,
504
- comment: decisionComment,
505
- decidedAt,
506
- decidedByMemberId: input.decidedByMemberId,
507
- returnToNodeId,
508
- signatureId: signature.id,
509
- taskId: task.id,
510
- transferToMemberId,
511
- }),
512
- );
513
-
514
- if (input.action === TaskDecisionActionEnum.TRANSFERRED) {
515
- await this.transferTask(
516
- manager,
517
- instance,
518
- claimedTask,
519
- decision,
520
- signature,
521
- transferToMemberId,
522
- decisionComment,
523
- decidedAt,
524
- claimedCandidate,
525
- );
526
-
527
- return decision;
528
- }
529
-
530
- const nextCandidateRows = await this.markTaskCandidateDecision({
531
- action: input.action,
532
- candidate: claimedCandidate,
533
- candidates: taskCandidates,
534
- decidedAt,
535
- manager,
536
- task: claimedTask,
537
- });
538
- const shouldCompleteTask = shouldCompleteTaskAfterDecision({
539
- action: input.action,
540
- candidates: nextCandidateRows,
541
- decisionPolicy: readDecisionPolicySnapshot(claimedTask),
542
- });
543
- const completedTask = shouldCompleteTask
544
- ? await taskRepository.save({
545
- ...claimedTask,
546
- completedAt: decidedAt,
547
- status: TaskStatusEnum.COMPLETED,
548
- })
549
- : await taskRepository.save({
550
- ...claimedTask,
551
- status: TaskStatusEnum.IN_PROGRESS,
552
- });
553
-
554
- await activityRepository.save(
555
- activityRepository.create({
556
- actorMemberId: input.decidedByMemberId,
557
- eventType: ActivityLogEventTypeEnum.TASK_DECIDED,
558
- instanceId: instance.id,
559
- nodeId: claimedTask.nodeId,
560
- payload: {
561
- action: input.action,
562
- comment: decisionComment,
563
- decisionId: decision.id,
564
- signatureId: signature.id,
565
- signedPayloadHash: signature.signedPayloadHash,
566
- },
567
- taskId: completedTask.id,
568
- }),
569
- );
570
-
571
- if (
572
- input.action === TaskDecisionActionEnum.APPROVED &&
573
- shouldCompleteTask
574
- ) {
575
- const token = await tokenRepository.findOne({
576
- where: { id: claimedTask.tokenId },
577
- });
578
-
579
- if (!token) {
580
- throw new NotFoundException(
581
- `Workflow token ${claimedTask.tokenId} was not found`,
582
- );
583
- }
584
-
585
- if (token.status !== WorkflowTokenStatusEnum.WAITING) {
586
- throw new ConflictException(
587
- `Workflow token ${token.id} is not waiting for a task decision`,
588
- );
589
- }
590
-
591
- const activeToken = await tokenRepository.save({
592
- ...token,
593
- consumedAt: null,
594
- status: WorkflowTokenStatusEnum.ACTIVE,
595
- });
596
-
597
- await this.advanceTokenToOutgoingNodes(
598
- manager,
599
- instance,
600
- activeToken,
601
- readWorkflowNodeOrThrow(
602
- instance.workflowSnapshot,
603
- claimedTask.nodeId,
604
- ),
605
- );
606
- await this.processRunningInstance(manager, instance);
607
-
608
- return decision;
609
- }
610
-
611
- if (input.action === TaskDecisionActionEnum.APPROVED) {
612
- return decision;
613
- }
614
-
615
- if (input.action === TaskDecisionActionEnum.RETURNED) {
616
- await this.returnInstanceToNode(
617
- manager,
618
- instance,
619
- completedTask,
620
- returnToNodeId,
621
- decidedAt,
622
- );
623
-
624
- return decision;
625
- }
626
-
627
- await this.rejectInstance(manager, instance, completedTask, decidedAt);
628
-
629
- return decision;
630
- },
631
- );
632
- }
633
-
634
- async cancelApprovalInstance(
635
- input: CancelApprovalInstanceInput,
636
- ): Promise<ApprovalInstanceEntity> {
637
- return this.approvalInstanceRepository.manager.transaction(
638
- async (manager): Promise<ApprovalInstanceEntity> => {
639
- await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
640
- input.instanceId,
641
- ]);
642
-
643
- const instanceRepository = manager.getRepository(
644
- ApprovalInstanceEntity,
645
- );
646
- const instance = await instanceRepository.findOne({
647
- where: { id: input.instanceId },
648
- });
649
-
650
- if (!instance) {
651
- throw new NotFoundException(
652
- `Approval instance ${input.instanceId} was not found`,
653
- );
654
- }
655
-
656
- if (instance.initiatorMemberId !== input.cancelledByMemberId) {
657
- throw new ConflictException(
658
- `Approval instance ${instance.id} can only be cancelled by its initiator`,
659
- );
660
- }
661
-
662
- if (
663
- instance.state !== ApprovalInstanceStateEnum.RUNNING &&
664
- instance.state !== ApprovalInstanceStateEnum.RETURNED
665
- ) {
666
- throw new ConflictException(
667
- `Approval instance ${instance.id} is not cancellable`,
668
- );
669
- }
670
-
671
- const cancelledAt = new Date();
672
- await this.consumeOpenRuntimeState(
673
- manager,
674
- instance,
675
- cancelledAt,
676
- TaskStatusEnum.CANCELLED,
677
- );
678
- const cancelledInstance = await instanceRepository.save({
679
- ...instance,
680
- completedAt: cancelledAt,
681
- state: ApprovalInstanceStateEnum.CANCELLED,
682
- });
683
-
684
- await manager.getRepository(ActivityLogEntity).save(
685
- manager.getRepository(ActivityLogEntity).create({
686
- actorMemberId: input.cancelledByMemberId,
687
- eventType: ActivityLogEventTypeEnum.INSTANCE_CANCELLED,
688
- instanceId: instance.id,
689
- nodeId: null,
690
- payload: {
691
- comment: input.comment?.trim() || null,
692
- },
693
- taskId: null,
694
- }),
695
- );
696
-
697
- return cancelledInstance;
698
- },
699
- );
700
- }
701
-
702
- async resubmitApprovalInstance(
703
- input: ResubmitApprovalInstanceInput,
704
- ): Promise<ApprovalInstanceEntity> {
705
- const formData = parseJsonObject(input.formDataJson, 'formDataJson');
706
-
707
- return this.approvalInstanceRepository.manager.transaction(
708
- async (manager): Promise<ApprovalInstanceEntity> => {
709
- await manager.query('SELECT pg_advisory_xact_lock(hashtext($1))', [
710
- input.instanceId,
711
- ]);
712
-
713
- const instanceRepository = manager.getRepository(
714
- ApprovalInstanceEntity,
715
- );
716
- const instance = await instanceRepository.findOne({
717
- where: { id: input.instanceId },
718
- });
719
-
720
- if (!instance) {
721
- throw new NotFoundException(
722
- `Approval instance ${input.instanceId} was not found`,
723
- );
724
- }
725
-
726
- if (instance.initiatorMemberId !== input.initiatorMemberId) {
727
- throw new ConflictException(
728
- `Approval instance ${instance.id} can only be resubmitted by its initiator`,
729
- );
730
- }
731
-
732
- if (instance.state !== ApprovalInstanceStateEnum.RETURNED) {
733
- throw new ConflictException(
734
- `Approval instance ${instance.id} is not returned`,
735
- );
736
- }
737
-
738
- validateSubmittedFormData(
739
- readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot),
740
- formData,
741
- );
742
-
743
- const resubmittedInstance = await instanceRepository.save({
744
- ...instance,
745
- completedAt: null,
746
- formData,
747
- state: ApprovalInstanceStateEnum.RUNNING,
748
- title: input.title?.trim() || instance.title,
749
- });
750
- await this.attachmentService.bindFormDataAttachmentsToInstance(
751
- manager,
752
- {
753
- formData,
754
- instanceId: instance.id,
755
- },
756
- );
757
- const returnedContext = await this.readLatestReturnedInstanceContext(
758
- manager,
759
- instance.id,
760
- );
761
- const runtimeInstance =
762
- returnedContext?.resubmitStrategy === 'FROM_RETURN_POINT'
763
- ? await this.resumeReturnedInstanceFromReturnPoint(
764
- manager,
765
- resubmittedInstance,
766
- returnedContext,
767
- new Date(),
768
- )
769
- : resubmittedInstance;
770
-
771
- await manager.getRepository(ActivityLogEntity).save(
772
- manager.getRepository(ActivityLogEntity).create({
773
- actorMemberId: input.initiatorMemberId,
774
- eventType: ActivityLogEventTypeEnum.INSTANCE_RESUBMITTED,
775
- instanceId: instance.id,
776
- nodeId: null,
777
- payload: {
778
- resubmitStrategy: returnedContext?.resubmitStrategy ?? 'RESTART',
779
- },
780
- taskId: null,
781
- }),
782
- );
783
- await this.processRunningInstance(manager, runtimeInstance);
784
-
785
- return runtimeInstance;
786
- },
787
- );
788
- }
789
-
790
- dryRunApprovalWorkflow(
791
- input: DryRunApprovalWorkflowInput,
792
- ): WorkflowDryRunResultObject {
793
- const workflowDefinition = parseWorkflowDefinition(
794
- input.workflowDefinitionJson,
795
- );
796
- const formData = parseJsonObject(input.formDataJson, 'formDataJson');
797
- const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
798
- ? parseJsonObject(
799
- input.initiatorMetadataSnapshotJson,
800
- 'initiatorMetadataSnapshotJson',
801
- )
802
- : readDefaultInitiatorMetadataSnapshot(input.initiatorMemberId);
803
-
804
- try {
805
- return Object.assign(new WorkflowDryRunResultObject(), {
806
- errors: [],
807
- steps: this.simulateWorkflow({
808
- formData,
809
- initiatorMemberId: input.initiatorMemberId,
810
- initiatorMetadataSnapshot,
811
- workflowDefinition,
812
- }),
813
- valid: true,
814
- });
815
- } catch (error: unknown) {
816
- return Object.assign(new WorkflowDryRunResultObject(), {
817
- errors: [error instanceof Error ? error.message : 'Dry run failed'],
818
- steps: [],
819
- valid: false,
820
- });
821
- }
822
- }
823
-
824
- private simulateWorkflow(
825
- input: DryRunSimulationInput,
826
- ): readonly WorkflowDryRunStepObject[] {
827
- const startNode = input.workflowDefinition.nodes.find(
828
- (node) => node.type === 'startEvent',
829
- );
830
-
831
- if (!startNode) {
832
- throw new BadRequestException('Workflow does not include a startEvent');
833
- }
834
-
835
- return this.simulateWorkflowPath({
836
- ...input,
837
- depth: 0,
838
- incomingEdge: null,
839
- lastDecision: null,
840
- nodeId: startNode.id,
841
- steps: [],
842
- visitedNodeIds: new Set(),
843
- });
844
- }
845
-
846
- private simulateWorkflowPath(
847
- input: DryRunPathInput,
848
- ): readonly WorkflowDryRunStepObject[] {
849
- if (input.depth > MAX_PROCESSING_STEPS) {
850
- throw new ConflictException('Dry run exceeded maximum processing steps');
851
- }
852
-
853
- const node = readWorkflowNodeOrThrow(
854
- input.workflowDefinition,
855
- input.nodeId,
856
- );
857
- const visitKey = `${input.nodeId}:${input.incomingEdge?.id ?? 'start'}`;
858
- const entryCondition = readNodeEntryCondition(node);
859
-
860
- if (input.visitedNodeIds.has(visitKey)) {
861
- return [
862
- ...input.steps,
863
- createDryRunStep({
864
- assigneeMemberId: null,
865
- edge: input.incomingEdge,
866
- edgeMatched: null,
867
- edgeReason: null,
868
- entryCondition,
869
- entryConditionMet: null,
870
- message: '偵測到循環路徑,已停止此分支。',
871
- node,
872
- status: 'STOPPED',
873
- stepIndex: input.steps.length,
874
- }),
875
- ];
876
- }
877
-
878
- const nextVisitedNodeIds = new Set([...input.visitedNodeIds, visitKey]);
879
- const entryConditionMet = this.conditionService.evaluateBoolean(
880
- entryCondition,
881
- buildDryRunExpressionContext(input, input.lastDecision),
882
- `workflow.nodes.${node.id}.data.entryCondition`,
883
- );
884
- const assigneeMemberId =
885
- entryConditionMet && node.type === 'userTask'
886
- ? this.resolveDryRunAssigneeMemberId(
887
- input,
888
- node.data.approverResolver,
889
- `workflow.nodes.${node.id}.data.approverResolver`,
890
- )
891
- : null;
892
- const nextLastDecision =
893
- entryConditionMet && node.type === 'userTask'
894
- ? {
895
- action: TaskDecisionActionEnum.APPROVED,
896
- assigneeMemberId,
897
- nodeId: node.id,
898
- }
899
- : input.lastDecision;
900
- const nextSteps = [
901
- ...input.steps,
902
- createDryRunStep({
903
- assigneeMemberId,
904
- edge: input.incomingEdge,
905
- edgeMatched: input.incomingEdge ? true : null,
906
- edgeReason: readDryRunEdgeReason(input.incomingEdge),
907
- entryCondition,
908
- entryConditionMet,
909
- message: readDryRunStepMessage(node, entryConditionMet),
910
- node,
911
- status: readDryRunStepStatus(node, entryConditionMet),
912
- stepIndex: input.steps.length,
913
- }),
914
- ];
915
-
916
- if (!entryConditionMet || node.type === 'endEvent') {
917
- if (node.type === 'endEvent') {
918
- return nextSteps;
919
- }
920
-
921
- return readOutgoingEdgesOrThrow(input.workflowDefinition, node.id).reduce(
922
- (steps, edge) =>
923
- this.simulateWorkflowPath({
924
- ...input,
925
- depth: input.depth + 1,
926
- incomingEdge: edge,
927
- lastDecision: nextLastDecision,
928
- nodeId: edge.target,
929
- steps,
930
- visitedNodeIds: nextVisitedNodeIds,
931
- }),
932
- nextSteps,
933
- );
934
- }
935
-
936
- const outgoingEdges = this.readDryRunAvailableOutgoingEdges(
937
- input,
938
- node,
939
- nextLastDecision,
940
- );
941
-
942
- return outgoingEdges.reduce(
943
- (steps, edge) =>
944
- this.simulateWorkflowPath({
945
- ...input,
946
- depth: input.depth + 1,
947
- incomingEdge: edge,
948
- lastDecision: nextLastDecision,
949
- nodeId: edge.target,
950
- steps,
951
- visitedNodeIds: nextVisitedNodeIds,
952
- }),
953
- nextSteps,
954
- );
955
- }
956
-
957
- private readDryRunAvailableOutgoingEdges(
958
- input: DryRunSimulationInput,
959
- node: WorkflowNode,
960
- lastDecision: Readonly<Record<string, unknown>> | null,
961
- ): readonly WorkflowEdge[] {
962
- if (node.type === 'endEvent') {
963
- return [];
964
- }
965
-
966
- const outgoingEdges = readOutgoingEdgesOrThrow(
967
- input.workflowDefinition,
968
- node.id,
969
- );
970
- const conditionalEdges = outgoingEdges.filter(
971
- (edge) => !edge.data.isDefault && edgeHasCondition(edge),
972
- );
973
-
974
- if (conditionalEdges.length === 0) {
975
- return outgoingEdges;
976
- }
977
-
978
- const matchingEdges = conditionalEdges.filter((edge) =>
979
- this.evaluateDryRunEdge(input, edge, lastDecision),
980
- );
981
-
982
- if (matchingEdges.length > 0) {
983
- return node.type === 'exclusiveGateway'
984
- ? [matchingEdges[0]]
985
- : matchingEdges;
986
- }
987
-
988
- const defaultEdges = outgoingEdges.filter((edge) => edge.data.isDefault);
989
-
990
- if (defaultEdges.length > 0) {
991
- return node.type === 'exclusiveGateway'
992
- ? [defaultEdges[0]]
993
- : defaultEdges;
994
- }
995
-
996
- throw new ConflictException(
997
- `Workflow node ${node.id} has no matching outgoing edge`,
998
- );
999
- }
1000
-
1001
- private evaluateDryRunEdge(
1002
- input: DryRunSimulationInput,
1003
- edge: WorkflowEdge,
1004
- lastDecision: Readonly<Record<string, unknown>> | null,
1005
- ): boolean {
1006
- const expression = edge.data.condition?.trim();
1007
-
1008
- if (expression) {
1009
- return this.conditionService.evaluateBoolean(
1010
- expression,
1011
- buildDryRunExpressionContext(input, lastDecision),
1012
- `workflow.edges.${edge.id}.data.condition`,
1013
- );
1014
- }
1015
-
1016
- return evaluateWorkflowEdgeCondition(edge, { formData: input.formData });
1017
- }
1018
-
1019
- private resolveDryRunAssigneeMemberId(
1020
- input: DryRunSimulationInput,
1021
- resolver: ApproverResolver,
1022
- label: string,
1023
- ): string {
1024
- if (resolver.type === 'DIRECT') {
1025
- return readMemberIdFromValue(resolver.memberIds, label);
1026
- }
1027
-
1028
- if (resolver.type === 'DYNAMIC_FORM') {
1029
- return readMemberIdFromValue(
1030
- readValueAtPath(input.formData, resolver.formPath),
1031
- `${label}.formPath`,
1032
- );
1033
- }
1034
-
1035
- if (resolver.type === 'EXPRESSION') {
1036
- return readMemberIdFromValue(
1037
- this.conditionService.evaluateValue(
1038
- resolver.expression,
1039
- buildDryRunExpressionContext(input, null),
1040
- `${label}.expression`,
1041
- ),
1042
- `${label}.expression`,
1043
- );
1044
- }
1045
-
1046
- if (resolver.type === 'POSITION') {
1047
- return `position:${resolver.positionId}`;
1048
- }
1049
-
1050
- if (resolver.type === 'ORG_UNIT_MEMBER') {
1051
- return `orgUnitMember:${resolver.orgUnitId}`;
1052
- }
1053
-
1054
- if (resolver.type === 'ORG_UNIT_POSITION') {
1055
- return `orgUnitPosition:${resolver.orgUnitId}:${resolver.positionId}`;
1056
- }
1057
-
1058
- if (resolver.type === 'ORG_UNIT_MANAGER') {
1059
- return `orgUnitManager:${resolver.orgUnitId}`;
1060
- }
1061
-
1062
- return readMemberIdFromValue(
1063
- readManagerMemberIdFromInitiatorSnapshot(
1064
- input.initiatorMetadataSnapshot,
1065
- resolver.levelsUp,
1066
- ),
1067
- label,
1068
- );
1069
- }
1070
-
1071
- async getApprovalInstance(
1072
- id: string,
1073
- scope?: WorkflowReadScope,
1074
- ): Promise<ApprovalInstanceEntity> {
1075
- const instance = await this.approvalInstanceRepository.findOne({
1076
- where: { id },
1077
- });
1078
-
1079
- if (!instance) {
1080
- throw new NotFoundException(`Approval instance ${id} was not found`);
1081
- }
1082
-
1083
- if (
1084
- scope &&
1085
- !canReadAllWorkflows(scope) &&
1086
- !(await this.isApprovalInstanceReadableByMember(instance, scope.memberId))
1087
- ) {
1088
- throw new NotFoundException(`Approval instance ${id} was not found`);
1089
- }
1090
-
1091
- return instance;
1092
- }
1093
-
1094
- async listApprovalInstances(
1095
- scope?: WorkflowReadScope,
1096
- options: ListApprovalInstancesOptions = {},
1097
- ): Promise<readonly ApprovalInstanceEntity[]> {
1098
- const queryBuilder = await this.createFilteredApprovalInstanceQueryBuilder(
1099
- scope,
1100
- options,
1101
- );
1102
-
1103
- if (!queryBuilder) {
1104
- return [];
1105
- }
1106
-
1107
- queryBuilder.orderBy('"approvalInstance"."created_at"', 'DESC');
1108
-
1109
- if (shouldPaginateList(options)) {
1110
- queryBuilder
1111
- .skip(readPageOffset(options.page, options.pageSize))
1112
- .take(normalizePageSize(options.pageSize));
1113
- }
1114
-
1115
- return queryBuilder.getMany();
1116
- }
1117
-
1118
- async countApprovalInstances(
1119
- scope?: WorkflowReadScope,
1120
- options: Omit<ListApprovalInstancesOptions, 'page' | 'pageSize'> = {},
1121
- ): Promise<number> {
1122
- const queryBuilder = await this.createFilteredApprovalInstanceQueryBuilder(
1123
- scope,
1124
- options,
1125
- );
1126
-
1127
- return queryBuilder ? queryBuilder.getCount() : 0;
1128
- }
1129
-
1130
- async readApprovalInstancePageInfo(
1131
- scope?: WorkflowReadScope,
1132
- options: ListApprovalInstancesOptions = {},
1133
- ): Promise<{
1134
- readonly hasNextPage: boolean;
1135
- readonly hasPreviousPage: boolean;
1136
- readonly page: number;
1137
- readonly pageSize: number;
1138
- readonly totalCount: number;
1139
- readonly totalPages: number;
1140
- }> {
1141
- const totalCount = await this.countApprovalInstances(scope, options);
1142
- const page = normalizePage(options.page);
1143
- const pageSize = normalizePageSize(options.pageSize);
1144
- const totalPages = Math.max(Math.ceil(totalCount / pageSize), 1);
1145
-
1146
- return {
1147
- hasNextPage: page < totalPages,
1148
- hasPreviousPage: page > 1,
1149
- page,
1150
- pageSize,
1151
- totalCount,
1152
- totalPages,
1153
- };
1154
- }
1155
-
1156
- private async createFilteredApprovalInstanceQueryBuilder(
1157
- scope?: WorkflowReadScope,
1158
- options: Omit<ListApprovalInstancesOptions, 'page' | 'pageSize'> = {},
1159
- ): Promise<SelectQueryBuilder<ApprovalInstanceEntity> | null> {
1160
- const queryBuilder =
1161
- this.approvalInstanceRepository.createQueryBuilder('approvalInstance');
1162
- const hasReadableInstances = this.applyReadableInstanceFilter(
1163
- queryBuilder,
1164
- scope,
1165
- options,
1166
- );
1167
-
1168
- if (!hasReadableInstances) {
1169
- return null;
1170
- }
1171
-
1172
- const templateId = normalizeText(options.templateId);
1173
-
1174
- if (options.state?.length) {
1175
- queryBuilder.andWhere('"approvalInstance"."state" IN (:...states)', {
1176
- states: [...options.state],
1177
- });
1178
- }
1179
-
1180
- if (templateId) {
1181
- queryBuilder.andWhere('"approvalInstance"."template_id" = :templateId', {
1182
- templateId,
1183
- });
1184
- }
1185
-
1186
- applyApprovalInstanceSearchFilter(queryBuilder, options.searchText);
1187
-
1188
- return queryBuilder;
1189
- }
1190
-
1191
- private applyReadableInstanceFilter(
1192
- queryBuilder: SelectQueryBuilder<ApprovalInstanceEntity>,
1193
- scope: WorkflowReadScope | undefined,
1194
- options: Omit<ListApprovalInstancesOptions, 'page' | 'pageSize'>,
1195
- ): boolean {
1196
- if (!scope) {
1197
- return true;
1198
- }
1199
-
1200
- const view = options.view ?? ApprovalInstanceListViewEnum.ALL;
1201
-
1202
- if (view === ApprovalInstanceListViewEnum.SENT) {
1203
- queryBuilder.andWhere(
1204
- '"approvalInstance"."initiator_member_id" = :readableMemberId',
1205
- {
1206
- readableMemberId: scope.memberId,
1207
- },
1208
- );
1209
-
1210
- return true;
1211
- }
1212
-
1213
- if (view === ApprovalInstanceListViewEnum.CC) {
1214
- queryBuilder.andWhere(
1215
- `
1216
- EXISTS (
1217
- SELECT 1
1218
- FROM "notifications" "readableNotification"
1219
- WHERE "readableNotification"."instance_id" = "approvalInstance"."id"
1220
- AND "readableNotification"."recipient_member_id" = :readableMemberId
1221
- )
1222
- `,
1223
- {
1224
- readableMemberId: scope.memberId,
1225
- },
1226
- );
1227
-
1228
- return true;
1229
- }
1230
-
1231
- if (canReadAllWorkflows(scope)) {
1232
- return true;
1233
- }
1234
-
1235
- queryBuilder.andWhere(
1236
- `
1237
- (
1238
- "approvalInstance"."initiator_member_id" = :readableMemberId
1239
- OR EXISTS (
1240
- SELECT 1
1241
- FROM "tasks" "readableTask"
1242
- WHERE "readableTask"."instance_id" = "approvalInstance"."id"
1243
- AND (
1244
- "readableTask"."assignee_member_id" = :readableMemberId
1245
- OR "readableTask"."original_assignee_member_id" = :readableMemberId
1246
- )
1247
- )
1248
- OR EXISTS (
1249
- SELECT 1
1250
- FROM "task_candidates" "readableCandidate"
1251
- INNER JOIN "tasks" "readableCandidateTask"
1252
- ON "readableCandidateTask"."id" = "readableCandidate"."task_id"
1253
- WHERE "readableCandidateTask"."instance_id" = "approvalInstance"."id"
1254
- AND (
1255
- "readableCandidate"."member_id" = :readableMemberId
1256
- OR "readableCandidate"."original_member_id" = :readableMemberId
1257
- )
1258
- )
1259
- OR EXISTS (
1260
- SELECT 1
1261
- FROM "task_decisions" "readableDecision"
1262
- INNER JOIN "tasks" "readableDecisionTask"
1263
- ON "readableDecisionTask"."id" = "readableDecision"."task_id"
1264
- WHERE "readableDecisionTask"."instance_id" = "approvalInstance"."id"
1265
- AND "readableDecision"."decided_by_member_id" = :readableMemberId
1266
- )
1267
- )
1268
- `,
1269
- {
1270
- readableMemberId: scope.memberId,
1271
- },
1272
- );
1273
-
1274
- return true;
1275
- }
1276
-
1277
- async readWorkflowDashboardSummary(
1278
- scope?: WorkflowReadScope,
1279
- options: WorkflowDashboardSummaryOptions = {},
1280
- ): Promise<{
1281
- readonly activeInstanceCount: number;
1282
- readonly completedInstanceCount: number;
1283
- readonly overdueTaskCount: number;
1284
- readonly pendingTaskCount: number;
1285
- readonly rejectedInstanceCount: number;
1286
- readonly totalInstanceCount: number;
1287
- }> {
1288
- const instances = (
1289
- await this.listApprovalInstances(scope, {
1290
- view: ApprovalInstanceListViewEnum.ALL,
1291
- })
1292
- ).filter((instance) => isInstanceWithinRange(instance, options));
1293
- const instanceIds = instances.map((instance) => instance.id);
1294
- const openTasks = instanceIds.length
1295
- ? await this.taskRepository.find({
1296
- where: {
1297
- instanceId: In(instanceIds),
1298
- status: In([TaskStatusEnum.PENDING, TaskStatusEnum.IN_PROGRESS]),
1299
- },
1300
- })
1301
- : [];
1302
- const now = Date.now();
1303
-
1304
- return {
1305
- activeInstanceCount: instances.filter(
1306
- (instance) => instance.state === ApprovalInstanceStateEnum.RUNNING,
1307
- ).length,
1308
- completedInstanceCount: instances.filter(
1309
- (instance) => instance.state === ApprovalInstanceStateEnum.APPROVED,
1310
- ).length,
1311
- overdueTaskCount: openTasks.filter(
1312
- (task) => task.slaDueAt && task.slaDueAt.getTime() <= now,
1313
- ).length,
1314
- pendingTaskCount: openTasks.length,
1315
- rejectedInstanceCount: instances.filter(
1316
- (instance) => instance.state === ApprovalInstanceStateEnum.REJECTED,
1317
- ).length,
1318
- totalInstanceCount: instances.length,
1319
- };
1320
- }
1321
-
1322
- async listLaunchableApprovalTemplates(
1323
- memberId: string,
1324
- ): Promise<readonly ApprovalTemplateEntity[]> {
1325
- const templates = await this.approvalTemplateRepository.find({
1326
- order: { updatedAt: 'DESC' },
1327
- });
1328
- const currentVersionIds = templates
1329
- .map((template) => template.currentVersionId)
1330
- .filter((versionId): versionId is string => Boolean(versionId));
1331
-
1332
- if (!currentVersionIds.length) {
1333
- return [];
1334
- }
1335
-
1336
- const versions = await this.approvalTemplateVersionRepository.find({
1337
- where: {
1338
- id: In(currentVersionIds),
1339
- status: ApprovalTemplateVersionStatusEnum.PUBLISHED,
1340
- },
1341
- });
1342
- const versionById = new Map(
1343
- versions.map(
1344
- (version): readonly [string, ApprovalTemplateVersionEntity] => [
1345
- version.id,
1346
- version,
1347
- ],
1348
- ),
1349
- );
1350
- const initiatorMetadataSnapshot =
1351
- await this.readDefaultInitiatorMetadataSnapshot(memberId);
1352
-
1353
- return templates.filter((template) => {
1354
- const currentVersion = template.currentVersionId
1355
- ? (versionById.get(template.currentVersionId) ?? null)
1356
- : null;
1357
-
1358
- if (!currentVersion?.formDefinitionVersionId) {
1359
- return false;
1360
- }
1361
-
1362
- return this.conditionService.evaluateBoolean(
1363
- currentVersion.initiatorPolicyCel,
1364
- buildInitiatorPolicyContext(initiatorMetadataSnapshot),
1365
- 'initiatorPolicyCel',
1366
- );
1367
- });
1368
- }
1369
-
1370
- async listWorkflowTokens(
1371
- instanceId: string,
1372
- scope?: WorkflowReadScope,
1373
- ): Promise<readonly WorkflowTokenEntity[]> {
1374
- await this.getApprovalInstance(instanceId, scope);
1375
-
1376
- return this.workflowTokenRepository.find({
1377
- order: { createdAt: 'ASC' },
1378
- where: { instanceId },
1379
- });
1380
- }
1381
-
1382
- async listTasks(
1383
- instanceId: string,
1384
- scope?: WorkflowReadScope,
1385
- ): Promise<readonly TaskEntity[]> {
1386
- await this.getApprovalInstance(instanceId, scope);
1387
-
1388
- return this.attachTaskCandidateSummaries(
1389
- await this.taskRepository.find({
1390
- order: { createdAt: 'ASC' },
1391
- where: { instanceId },
1392
- }),
1393
- );
1394
- }
1395
-
1396
- async listInboxTasks(
1397
- assigneeMemberId: string,
1398
- ): Promise<readonly TaskEntity[]> {
1399
- const directTasks = await this.taskRepository.find({
1400
- order: { createdAt: 'DESC' },
1401
- where: [
1402
- {
1403
- assigneeMemberId,
1404
- status: In([TaskStatusEnum.PENDING, TaskStatusEnum.IN_PROGRESS]),
1405
- },
1406
- ],
1407
- });
1408
- const candidateRows = await this.taskCandidateRepository.find({
1409
- where: {
1410
- memberId: assigneeMemberId,
1411
- status: In([
1412
- TaskCandidateStatusEnum.PENDING,
1413
- TaskCandidateStatusEnum.CLAIMED,
1414
- ]),
1415
- },
1416
- });
1417
- const candidateTasks = candidateRows.length
1418
- ? await this.taskRepository.find({
1419
- order: { createdAt: 'DESC' },
1420
- where: {
1421
- id: In(candidateRows.map((candidate) => candidate.taskId)),
1422
- status: In([TaskStatusEnum.PENDING, TaskStatusEnum.IN_PROGRESS]),
1423
- },
1424
- })
1425
- : [];
1426
-
1427
- return this.attachTaskCandidateSummaries(
1428
- uniqueTasksById([...directTasks, ...candidateTasks]),
1429
- );
1430
- }
1431
-
1432
- async listApprovalHistoryTasks(
1433
- assigneeMemberId: string,
1434
- ): Promise<readonly TaskEntity[]> {
1435
- const directTasks = await this.taskRepository.find({
1436
- order: { completedAt: 'DESC', createdAt: 'DESC' },
1437
- take: DEFAULT_APPROVAL_HISTORY_TASK_LIMIT,
1438
- where: { assigneeMemberId, status: TaskStatusEnum.COMPLETED },
1439
- });
1440
- const candidateRows = await this.taskCandidateRepository.find({
1441
- where: {
1442
- memberId: assigneeMemberId,
1443
- status: TaskCandidateStatusEnum.COMPLETED,
1444
- },
1445
- });
1446
- const candidateTasks = candidateRows.length
1447
- ? await this.taskRepository.find({
1448
- order: { completedAt: 'DESC', createdAt: 'DESC' },
1449
- take: DEFAULT_APPROVAL_HISTORY_TASK_LIMIT,
1450
- where: {
1451
- id: In(candidateRows.map((candidate) => candidate.taskId)),
1452
- status: TaskStatusEnum.COMPLETED,
1453
- },
1454
- })
1455
- : [];
1456
-
1457
- return this.attachTaskCandidateSummaries(
1458
- uniqueTasksById([...directTasks, ...candidateTasks]),
1459
- );
1460
- }
1461
-
1462
- async listTaskDecisions(
1463
- taskId: string,
1464
- scope?: WorkflowReadScope,
1465
- ): Promise<readonly TaskDecisionEntity[]> {
1466
- await this.assertTaskReadable(taskId, scope);
1467
-
1468
- return this.taskDecisionRepository.find({
1469
- order: { decidedAt: 'ASC' },
1470
- where: { taskId },
1471
- });
1472
- }
1473
-
1474
- async listTaskCandidates(
1475
- taskId: string,
1476
- scope?: WorkflowReadScope,
1477
- ): Promise<readonly TaskCandidateEntity[]> {
1478
- await this.assertTaskReadable(taskId, scope);
1479
-
1480
- return this.taskCandidateRepository.find({
1481
- order: { createdAt: 'ASC', id: 'ASC' },
1482
- where: { taskId },
1483
- });
1484
- }
1485
-
1486
- private async attachTaskCandidateSummaries(
1487
- tasks: readonly TaskEntity[],
1488
- ): Promise<readonly TaskEntity[]> {
1489
- if (!tasks.length) {
1490
- return tasks;
1491
- }
1492
-
1493
- const candidates = await this.taskCandidateRepository.find({
1494
- order: { createdAt: 'ASC', id: 'ASC' },
1495
- where: { taskId: In(tasks.map((task) => task.id)) },
1496
- });
1497
- const candidatesByTaskId = candidates.reduce<
1498
- ReadonlyMap<string, readonly string[]>
1499
- >((currentMap, candidate) => {
1500
- const nextMap = new Map(currentMap);
1501
- const memberIds = nextMap.get(candidate.taskId) ?? [];
1502
-
1503
- nextMap.set(candidate.taskId, [...memberIds, candidate.memberId]);
1504
-
1505
- return nextMap;
1506
- }, new Map<string, readonly string[]>());
1507
-
1508
- return tasks.map((task) =>
1509
- Object.assign(new TaskEntity(), task, {
1510
- candidateMemberIds: candidatesByTaskId.get(task.id) ?? [],
1511
- }),
1512
- );
1513
- }
1514
-
1515
- private async readDefaultInitiatorMetadataSnapshot(
1516
- memberId: string,
1517
- ): Promise<Readonly<Record<string, unknown>>> {
1518
- const today = toDateOnlyString(new Date());
1519
- const memberships = (
1520
- await this.approvalInstanceRepository.manager
1521
- .getRepository(MembershipEntity)
1522
- .find({ where: { memberId } })
1523
- )
1524
- .filter((membership) => isDateRangeActive(membership, today))
1525
- .sort(compareMembership);
1526
- const primaryMembership = memberships[0] ?? null;
1527
- const positionIds = uniqueTexts(
1528
- memberships
1529
- .map((membership) => membership.positionId)
1530
- .filter((positionId): positionId is string => Boolean(positionId)),
1531
- );
1532
-
1533
- return {
1534
- customFields: {},
1535
- memberId,
1536
- orgUnitIds: uniqueTexts(
1537
- memberships.map((membership) => membership.orgUnitId),
1538
- ),
1539
- positionId: primaryMembership?.positionId ?? null,
1540
- positionIds,
1541
- primaryOrgUnitId: primaryMembership?.orgUnitId ?? null,
1542
- };
1543
- }
1544
-
1545
- async listActivityLogs(
1546
- instanceId: string,
1547
- scope?: WorkflowReadScope,
1548
- ): Promise<readonly ActivityLogEntity[]> {
1549
- await this.getApprovalInstance(instanceId, scope);
1550
-
1551
- return this.activityLogRepository.find({
1552
- order: { createdAt: 'ASC' },
1553
- where: { instanceId },
1554
- });
1555
- }
1556
-
1557
- private async assertTaskReadable(
1558
- taskId: string,
1559
- scope?: WorkflowReadScope,
1560
- ): Promise<TaskEntity> {
1561
- const task = await this.taskRepository.findOne({ where: { id: taskId } });
1562
-
1563
- if (!task) {
1564
- throw new NotFoundException(`Task ${taskId} was not found`);
1565
- }
1566
-
1567
- await this.getApprovalInstance(task.instanceId, scope);
1568
-
1569
- return task;
1570
- }
1571
-
1572
- private async isApprovalInstanceReadableByMember(
1573
- instance: ApprovalInstanceEntity,
1574
- memberId: string,
1575
- ): Promise<boolean> {
1576
- if (instance.initiatorMemberId === memberId) {
1577
- return true;
1578
- }
1579
-
1580
- const tasks = await this.taskRepository.find({
1581
- where: { instanceId: instance.id },
1582
- });
1583
-
1584
- if (tasks.some((task) => isTaskRelatedToMember(task, memberId))) {
1585
- return true;
1586
- }
1587
-
1588
- const taskIds = tasks.map((task) => task.id);
1589
-
1590
- if (!taskIds.length) {
1591
- return false;
1592
- }
1593
-
1594
- const candidates = await this.taskCandidateRepository.find({
1595
- where: { taskId: In(taskIds) },
1596
- });
1597
-
1598
- if (
1599
- candidates.some(
1600
- (candidate) =>
1601
- candidate.memberId === memberId ||
1602
- candidate.originalMemberId === memberId,
1603
- )
1604
- ) {
1605
- return true;
1606
- }
1607
-
1608
- const decisions = await this.taskDecisionRepository.find({
1609
- where: { taskId: In(taskIds) },
1610
- });
1611
-
1612
- return decisions.some(
1613
- (decision) => decision.decidedByMemberId === memberId,
1614
- );
1615
- }
1616
-
1617
- private async processActiveToken(
1618
- manager: EntityManager,
1619
- instance: ApprovalInstanceEntity,
1620
- token: WorkflowTokenEntity,
1621
- ): Promise<void> {
1622
- const node = readWorkflowNodeOrThrow(
1623
- instance.workflowSnapshot,
1624
- token.currentNodeId,
1625
- );
1626
-
1627
- if (!(await this.isNodeEntryReady(manager, instance, token, node))) {
1628
- return;
1629
- }
1630
-
1631
- if (!this.isNodeEntryConditionMet(instance, node)) {
1632
- await this.skipNodeByEntryCondition(manager, instance, token, node);
1633
-
1634
- return;
1635
- }
1636
-
1637
- if (node.type === 'startEvent') {
1638
- await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
1639
-
1640
- return;
1641
- }
1642
-
1643
- if (node.type === 'endEvent') {
1644
- await this.completeTokenAtEndNode(manager, instance, token, node);
1645
-
1646
- return;
1647
- }
1648
-
1649
- if (node.type === 'userTask') {
1650
- await this.createUserTaskForToken(manager, instance, token, node);
1651
-
1652
- return;
1653
- }
1654
-
1655
- if (node.type === 'serviceTask') {
1656
- await this.executeServiceTask(manager, instance, token, node);
1657
-
1658
- return;
1659
- }
1660
-
1661
- if (node.type === 'exclusiveGateway') {
1662
- await this.processExclusiveGateway(manager, instance, token, node);
1663
-
1664
- return;
1665
- }
1666
-
1667
- if (node.type === 'parallelGateway') {
1668
- await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
1669
-
1670
- return;
1671
- }
1672
-
1673
- return;
1674
- }
1675
-
1676
- private async processRunningInstance(
1677
- manager: EntityManager,
1678
- instance: ApprovalInstanceEntity,
1679
- ): Promise<void> {
1680
- for (let step = 0; step < MAX_PROCESSING_STEPS; step += 1) {
1681
- const token = await manager.getRepository(WorkflowTokenEntity).findOne({
1682
- order: { createdAt: 'ASC' },
1683
- where: {
1684
- instanceId: instance.id,
1685
- status: WorkflowTokenStatusEnum.ACTIVE,
1686
- },
1687
- });
1688
-
1689
- if (!token) {
1690
- await this.completeInstanceIfNoOpenRuntimeState(manager, instance);
1691
-
1692
- return;
1693
- }
1694
-
1695
- await this.processActiveToken(manager, instance, token);
1696
- }
1697
-
1698
- throw new ConflictException(
1699
- `Approval instance ${instance.id} exceeded maximum processing steps`,
1700
- );
1701
- }
1702
-
1703
- private async completeInstanceIfNoOpenRuntimeState(
1704
- manager: EntityManager,
1705
- instance: ApprovalInstanceEntity,
1706
- ): Promise<void> {
1707
- if (instance.state !== ApprovalInstanceStateEnum.RUNNING) {
1708
- return;
1709
- }
1710
-
1711
- const waitingTokens = await manager
1712
- .getRepository(WorkflowTokenEntity)
1713
- .find({
1714
- where: {
1715
- instanceId: instance.id,
1716
- status: WorkflowTokenStatusEnum.WAITING,
1717
- },
1718
- });
1719
-
1720
- if (waitingTokens.length > 0) {
1721
- return;
1722
- }
1723
-
1724
- const completedAt = new Date();
1725
- const completedInstance = await manager
1726
- .getRepository(ApprovalInstanceEntity)
1727
- .save({
1728
- ...instance,
1729
- completedAt,
1730
- state: ApprovalInstanceStateEnum.APPROVED,
1731
- });
1732
-
1733
- await this.notificationService.createInstanceCompletedNotification({
1734
- instance: completedInstance,
1735
- manager,
1736
- });
1737
- }
1738
-
1739
- private async isNodeEntryReady(
1740
- manager: EntityManager,
1741
- instance: ApprovalInstanceEntity,
1742
- token: WorkflowTokenEntity,
1743
- node: WorkflowNode,
1744
- ): Promise<boolean> {
1745
- if (node.type === 'startEvent') {
1746
- return true;
1747
- }
1748
-
1749
- const incomingEdges = readIncomingEdges(instance.workflowSnapshot, node.id);
1750
-
1751
- if (incomingEdges.length < 2) {
1752
- return true;
1753
- }
1754
-
1755
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
1756
- const activityRepository = manager.getRepository(ActivityLogEntity);
1757
- const activeOrWaitingTokens = (
1758
- await tokenRepository.find({
1759
- where: { instanceId: instance.id },
1760
- })
1761
- ).filter(
1762
- (candidate) =>
1763
- candidate.currentNodeId === node.id &&
1764
- (candidate.status === WorkflowTokenStatusEnum.ACTIVE ||
1765
- candidate.status === WorkflowTokenStatusEnum.WAITING),
1766
- );
1767
- const arrivedTokens = activeOrWaitingTokens.some(
1768
- (candidate) => candidate.id === token.id,
1769
- )
1770
- ? activeOrWaitingTokens
1771
- : [...activeOrWaitingTokens, token];
1772
-
1773
- if ((node.data.triggerMode ?? 'AND') === 'OR') {
1774
- await this.cancelAlternativeTokensForOrTrigger(
1775
- manager,
1776
- instance,
1777
- token,
1778
- node.id,
1779
- );
1780
- await this.consumeJoinedTokens(
1781
- manager,
1782
- arrivedTokens.filter((candidate) => candidate.id !== token.id),
1783
- );
1784
-
1785
- return true;
1786
- }
1787
-
1788
- const pendingTokensThatMayReachNode = (
1789
- await tokenRepository.find({
1790
- where: { instanceId: instance.id },
1791
- })
1792
- ).filter(
1793
- (candidate) =>
1794
- candidate.id !== token.id &&
1795
- !arrivedTokens.some(
1796
- (arrivedToken) => arrivedToken.id === candidate.id,
1797
- ) &&
1798
- (candidate.status === WorkflowTokenStatusEnum.ACTIVE ||
1799
- candidate.status === WorkflowTokenStatusEnum.WAITING) &&
1800
- hasPathToNode(
1801
- instance.workflowSnapshot,
1802
- candidate.currentNodeId,
1803
- node.id,
1804
- ),
1805
- );
1806
-
1807
- if (
1808
- arrivedTokens.length < incomingEdges.length &&
1809
- pendingTokensThatMayReachNode.length > 0
1810
- ) {
1811
- await tokenRepository.save({
1812
- ...token,
1813
- status: WorkflowTokenStatusEnum.WAITING,
1814
- });
1815
- await activityRepository.save(
1816
- activityRepository.create({
1817
- actorMemberId: null,
1818
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
1819
- instanceId: instance.id,
1820
- nodeId: node.id,
1821
- payload: {
1822
- arrivedCount: arrivedTokens.length,
1823
- requiredCount: incomingEdges.length,
1824
- tokenId: token.id,
1825
- triggerMode: 'AND',
1826
- },
1827
- taskId: null,
1828
- }),
1829
- );
1830
-
1831
- return false;
1832
- }
1833
-
1834
- await this.consumeJoinedTokens(
1835
- manager,
1836
- arrivedTokens.filter((candidate) => candidate.id !== token.id),
1837
- );
1838
-
1839
- return true;
1840
- }
1841
-
1842
- private async advanceTokenToOutgoingNodes(
1843
- manager: EntityManager,
1844
- instance: ApprovalInstanceEntity,
1845
- token: WorkflowTokenEntity,
1846
- node: WorkflowNode,
1847
- ): Promise<void> {
1848
- const edges = await this.readAvailableOutgoingEdges(
1849
- manager,
1850
- instance,
1851
- node,
1852
- );
1853
-
1854
- if (edges.length === 1) {
1855
- await this.advanceTokenAlongEdge(
1856
- manager,
1857
- instance,
1858
- token,
1859
- node,
1860
- edges[0],
1861
- );
1862
-
1863
- return;
1864
- }
1865
-
1866
- await this.forkTokenAlongEdges(manager, instance, token, node, edges);
1867
- }
1868
-
1869
- private isNodeEntryConditionMet(
1870
- instance: ApprovalInstanceEntity,
1871
- node: WorkflowNode,
1872
- ): boolean {
1873
- const entryCondition = readNodeEntryCondition(node);
1874
-
1875
- return this.conditionService.evaluateBoolean(
1876
- entryCondition,
1877
- buildWorkflowExpressionContext(instance),
1878
- `workflow.nodes.${node.id}.data.entryCondition`,
1879
- );
1880
- }
1881
-
1882
- private async skipNodeByEntryCondition(
1883
- manager: EntityManager,
1884
- instance: ApprovalInstanceEntity,
1885
- token: WorkflowTokenEntity,
1886
- node: WorkflowNode,
1887
- ): Promise<void> {
1888
- await manager.getRepository(ActivityLogEntity).save(
1889
- manager.getRepository(ActivityLogEntity).create({
1890
- actorMemberId: null,
1891
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
1892
- instanceId: instance.id,
1893
- nodeId: node.id,
1894
- payload: {
1895
- skippedByEntryCondition: true,
1896
- tokenId: token.id,
1897
- },
1898
- taskId: null,
1899
- }),
1900
- );
1901
- await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
1902
- }
1903
-
1904
- private async readAvailableOutgoingEdges(
1905
- manager: EntityManager,
1906
- instance: ApprovalInstanceEntity,
1907
- node: WorkflowNode,
1908
- ): Promise<readonly WorkflowEdge[]> {
1909
- const outgoingEdges = readOutgoingEdgesOrThrow(
1910
- instance.workflowSnapshot,
1911
- node.id,
1912
- );
1913
- const conditionalEdges = outgoingEdges.filter(
1914
- (edge) => !edge.data.isDefault && edgeHasCondition(edge),
1915
- );
1916
-
1917
- if (conditionalEdges.length === 0) {
1918
- return outgoingEdges;
1919
- }
1920
-
1921
- const matchingEdges = (
1922
- await Promise.all(
1923
- conditionalEdges.map(
1924
- async (edge): Promise<WorkflowEdge | null> =>
1925
- (await this.evaluateWorkflowEdge(manager, instance, node, edge))
1926
- ? edge
1927
- : null,
1928
- ),
1929
- )
1930
- ).filter((edge): edge is WorkflowEdge => edge !== null);
1931
-
1932
- if (matchingEdges.length > 0) {
1933
- return node.type === 'exclusiveGateway'
1934
- ? [matchingEdges[0]]
1935
- : matchingEdges;
1936
- }
1937
-
1938
- const defaultEdges = outgoingEdges.filter((edge) => edge.data.isDefault);
1939
-
1940
- if (defaultEdges.length > 0) {
1941
- return node.type === 'exclusiveGateway'
1942
- ? [defaultEdges[0]]
1943
- : defaultEdges;
1944
- }
1945
-
1946
- throw new ConflictException(
1947
- `Workflow node ${node.id} has no matching outgoing edge`,
1948
- );
1949
- }
1950
-
1951
- private async evaluateWorkflowEdge(
1952
- manager: EntityManager,
1953
- instance: ApprovalInstanceEntity,
1954
- node: WorkflowNode,
1955
- edge: WorkflowEdge,
1956
- ): Promise<boolean> {
1957
- const expression = edge.data.condition?.trim();
1958
-
1959
- if (expression) {
1960
- return this.conditionService.evaluateBoolean(
1961
- expression,
1962
- {
1963
- ...buildWorkflowExpressionContext(instance),
1964
- lastDecision: await this.readLastDecisionContext(
1965
- manager,
1966
- instance.id,
1967
- node.id,
1968
- ),
1969
- },
1970
- `workflow.edges.${edge.id}.data.condition`,
1971
- );
1972
- }
1973
-
1974
- return evaluateWorkflowEdgeCondition(edge, { formData: instance.formData });
1975
- }
1976
-
1977
- private async readLastDecisionContext(
1978
- manager: EntityManager,
1979
- instanceId: string,
1980
- nodeId: string,
1981
- ): Promise<Readonly<Record<string, unknown>> | null> {
1982
- const tasks = await manager.getRepository(TaskEntity).find({
1983
- where: { instanceId, nodeId },
1984
- });
1985
- const taskIds = tasks.map((task) => task.id);
1986
-
1987
- if (taskIds.length === 0) {
1988
- return null;
1989
- }
1990
-
1991
- const decision = (
1992
- await manager.getRepository(TaskDecisionEntity).find({
1993
- order: { decidedAt: 'DESC' },
1994
- where: { taskId: In(taskIds) },
1995
- })
1996
- )[0];
1997
-
1998
- if (!decision) {
1999
- return null;
2000
- }
2001
-
2002
- return {
2003
- action: decision.action,
2004
- comment: decision.comment,
2005
- decidedAt: decision.decidedAt.toISOString(),
2006
- decidedByMemberId: decision.decidedByMemberId,
2007
- returnToNodeId: decision.returnToNodeId,
2008
- taskId: decision.taskId,
2009
- transferToMemberId: decision.transferToMemberId,
2010
- };
2011
- }
2012
-
2013
- private async advanceTokenAlongEdge(
2014
- manager: EntityManager,
2015
- instance: ApprovalInstanceEntity,
2016
- token: WorkflowTokenEntity,
2017
- node: WorkflowNode,
2018
- edge: WorkflowEdge,
2019
- ): Promise<void> {
2020
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
2021
- const activityRepository = manager.getRepository(ActivityLogEntity);
2022
- const nextToken = await tokenRepository.save({
2023
- ...token,
2024
- currentNodeId: edge.target,
2025
- });
2026
-
2027
- await activityRepository.save(
2028
- activityRepository.create({
2029
- actorMemberId: null,
2030
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
2031
- instanceId: instance.id,
2032
- nodeId: edge.target,
2033
- payload: {
2034
- edgeId: edge.id,
2035
- fromNodeId: node.id,
2036
- tokenId: nextToken.id,
2037
- toNodeId: edge.target,
2038
- },
2039
- taskId: null,
2040
- }),
2041
- );
2042
- }
2043
-
2044
- private async forkTokenAlongEdges(
2045
- manager: EntityManager,
2046
- instance: ApprovalInstanceEntity,
2047
- token: WorkflowTokenEntity,
2048
- node: WorkflowNode,
2049
- edges: readonly WorkflowEdge[],
2050
- ): Promise<void> {
2051
- const forkedAt = new Date();
2052
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
2053
- const activityRepository = manager.getRepository(ActivityLogEntity);
2054
-
2055
- await tokenRepository.save({
2056
- ...token,
2057
- consumedAt: forkedAt,
2058
- status: WorkflowTokenStatusEnum.CONSUMED,
2059
- });
2060
-
2061
- const childTokens = await tokenRepository.save(
2062
- edges.map((edge) =>
2063
- tokenRepository.create({
2064
- consumedAt: null,
2065
- currentNodeId: edge.target,
2066
- instanceId: instance.id,
2067
- parentTokenId: token.id,
2068
- status: WorkflowTokenStatusEnum.ACTIVE,
2069
- }),
2070
- ),
2071
- );
2072
-
2073
- await activityRepository.save([
2074
- activityRepository.create({
2075
- actorMemberId: null,
2076
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
2077
- instanceId: instance.id,
2078
- nodeId: node.id,
2079
- payload: {
2080
- edgeIds: edges.map((edge) => edge.id),
2081
- fromNodeId: node.id,
2082
- tokenId: token.id,
2083
- toNodeIds: edges.map((edge) => edge.target),
2084
- },
2085
- taskId: null,
2086
- }),
2087
- ...childTokens.map((childToken, index) =>
2088
- activityRepository.create({
2089
- actorMemberId: null,
2090
- eventType: ActivityLogEventTypeEnum.TOKEN_CREATED,
2091
- instanceId: instance.id,
2092
- nodeId: childToken.currentNodeId,
2093
- payload: {
2094
- edgeId: edges[index]?.id,
2095
- parentTokenId: token.id,
2096
- tokenId: childToken.id,
2097
- },
2098
- taskId: null,
2099
- }),
2100
- ),
2101
- ]);
2102
- }
2103
-
2104
- private async completeTokenAtEndNode(
2105
- manager: EntityManager,
2106
- instance: ApprovalInstanceEntity,
2107
- token: WorkflowTokenEntity,
2108
- node: Extract<WorkflowNode, { readonly type: 'endEvent' }>,
2109
- ): Promise<void> {
2110
- const completedAt = new Date();
2111
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
2112
- const instanceRepository = manager.getRepository(ApprovalInstanceEntity);
2113
- const activityRepository = manager.getRepository(ActivityLogEntity);
2114
- const endState = node.data.endState ?? 'APPROVED';
2115
- const instanceState =
2116
- endState === 'REJECTED'
2117
- ? ApprovalInstanceStateEnum.REJECTED
2118
- : ApprovalInstanceStateEnum.APPROVED;
2119
-
2120
- await tokenRepository.save({
2121
- ...token,
2122
- consumedAt: completedAt,
2123
- status: WorkflowTokenStatusEnum.CONSUMED,
2124
- });
2125
- const hasOpenToken = (
2126
- await tokenRepository.find({
2127
- where: { instanceId: instance.id },
2128
- })
2129
- ).some(
2130
- (candidate) =>
2131
- candidate.id !== token.id &&
2132
- (candidate.status === WorkflowTokenStatusEnum.ACTIVE ||
2133
- candidate.status === WorkflowTokenStatusEnum.WAITING),
2134
- );
2135
-
2136
- if (!hasOpenToken) {
2137
- const completedInstance = await instanceRepository.save({
2138
- ...instance,
2139
- completedAt,
2140
- state: instanceState,
2141
- });
2142
-
2143
- if (instanceState === ApprovalInstanceStateEnum.APPROVED) {
2144
- await this.notificationService.createInstanceCompletedNotification({
2145
- instance: completedInstance,
2146
- manager,
2147
- });
2148
- }
2149
- }
2150
- await activityRepository.save(
2151
- activityRepository.create({
2152
- actorMemberId: null,
2153
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
2154
- instanceId: instance.id,
2155
- nodeId: node.id,
2156
- payload: {
2157
- endState,
2158
- instanceState,
2159
- tokenId: token.id,
2160
- },
2161
- taskId: null,
2162
- }),
2163
- );
2164
- }
2165
-
2166
- private async createUserTaskForToken(
2167
- manager: EntityManager,
2168
- instance: ApprovalInstanceEntity,
2169
- token: WorkflowTokenEntity,
2170
- node: UserTaskNode,
2171
- ): Promise<void> {
2172
- const taskRepository = manager.getRepository(TaskEntity);
2173
- const taskCandidateRepository = manager.getRepository(TaskCandidateEntity);
2174
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
2175
- const activityRepository = manager.getRepository(ActivityLogEntity);
2176
- const existingTask = await taskRepository.findOne({
2177
- where: {
2178
- nodeId: node.id,
2179
- tokenId: token.id,
2180
- },
2181
- });
2182
-
2183
- if (existingTask) {
2184
- await tokenRepository.save({
2185
- ...token,
2186
- status: WorkflowTokenStatusEnum.WAITING,
2187
- });
2188
-
2189
- return;
2190
- }
2191
-
2192
- const candidates = await this.resolveRuntimeTaskCandidates(
2193
- manager,
2194
- instance,
2195
- node,
2196
- );
2197
- const primaryCandidate = candidates[0];
2198
- const now = new Date();
2199
- const task = await taskRepository.save(
2200
- taskRepository.create({
2201
- assigneeMemberId:
2202
- candidates.length === 1 ? primaryCandidate.memberId : null,
2203
- assignmentType:
2204
- candidates.length === 1
2205
- ? TaskAssignmentTypeEnum.DIRECT_MEMBER
2206
- : TaskAssignmentTypeEnum.CANDIDATE_GROUP,
2207
- completedAt: null,
2208
- createdAt: now,
2209
- decisionPolicySnapshot: node.data.decisionPolicy,
2210
- delegationChain:
2211
- candidates.length === 1 ? primaryCandidate.delegationChain : [],
2212
- instanceId: instance.id,
2213
- nodeId: node.id,
2214
- openedAt: null,
2215
- originalAssigneeMemberId:
2216
- candidates.length === 1 ? primaryCandidate.originalMemberId : null,
2217
- slaDueAt: calculateTaskSlaDueAt({ node, now }),
2218
- status: TaskStatusEnum.PENDING,
2219
- tokenId: token.id,
2220
- }),
2221
- );
2222
- const savedCandidates = await taskCandidateRepository.save(
2223
- candidates.map((candidate) =>
2224
- taskCandidateRepository.create({
2225
- claimedAt: null,
2226
- createdAt: now,
2227
- decidedAt: null,
2228
- delegationChain: candidate.delegationChain,
2229
- memberId: candidate.memberId,
2230
- originalMemberId: candidate.originalMemberId,
2231
- sourceType: candidate.sourceType,
2232
- status: TaskCandidateStatusEnum.PENDING,
2233
- taskId: task.id,
2234
- }),
2235
- ),
2236
- );
2237
- task.candidateMemberIds = savedCandidates.map(
2238
- (candidate) => candidate.memberId,
2239
- );
2240
- const singleCandidate =
2241
- savedCandidates.length === 1 ? savedCandidates[0] : null;
2242
-
2243
- await tokenRepository.save({
2244
- ...token,
2245
- status: WorkflowTokenStatusEnum.WAITING,
2246
- });
2247
- await activityRepository.save(
2248
- activityRepository.create({
2249
- actorMemberId: null,
2250
- eventType: ActivityLogEventTypeEnum.TASK_CREATED,
2251
- instanceId: instance.id,
2252
- nodeId: node.id,
2253
- payload: {
2254
- assignmentType: task.assignmentType,
2255
- assigneeMemberId: singleCandidate?.memberId ?? null,
2256
- candidateMemberIds: savedCandidates.map(
2257
- (candidate) => candidate.memberId,
2258
- ),
2259
- originalAssigneeMemberId: singleCandidate?.originalMemberId ?? null,
2260
- tokenId: token.id,
2261
- },
2262
- taskId: task.id,
2263
- }),
2264
- );
2265
- await savedCandidates.reduce<Promise<void>>(
2266
- async (previous, candidate): Promise<void> => {
2267
- await previous;
2268
- await this.notificationService.createTaskAssignedNotification({
2269
- instance,
2270
- manager,
2271
- node,
2272
- task: Object.assign(new TaskEntity(), task, {
2273
- assigneeMemberId: candidate.memberId,
2274
- delegationChain: candidate.delegationChain,
2275
- originalAssigneeMemberId: candidate.originalMemberId,
2276
- }),
2277
- });
2278
- },
2279
- Promise.resolve(),
2280
- );
2281
- }
2282
-
2283
- private async markTaskCandidateDecision({
2284
- action,
2285
- candidate,
2286
- candidates,
2287
- decidedAt,
2288
- manager,
2289
- task,
2290
- }: {
2291
- readonly action: TaskDecisionActionEnum;
2292
- readonly candidate: TaskCandidateEntity | null;
2293
- readonly candidates: readonly TaskCandidateEntity[];
2294
- readonly decidedAt: Date;
2295
- readonly manager: EntityManager;
2296
- readonly task: TaskEntity;
2297
- }): Promise<readonly TaskCandidateEntity[]> {
2298
- const taskCandidateRepository = manager.getRepository(TaskCandidateEntity);
2299
-
2300
- if (!candidate && candidates.length === 0 && task.assigneeMemberId) {
2301
- const legacyCandidate = await taskCandidateRepository.save(
2302
- taskCandidateRepository.create({
2303
- claimedAt: task.openedAt ?? decidedAt,
2304
- createdAt: task.createdAt,
2305
- decidedAt,
2306
- delegationChain: task.delegationChain,
2307
- memberId: task.assigneeMemberId,
2308
- originalMemberId:
2309
- task.originalAssigneeMemberId ?? task.assigneeMemberId,
2310
- sourceType: 'DIRECT',
2311
- status: TaskCandidateStatusEnum.COMPLETED,
2312
- taskId: task.id,
2313
- }),
2314
- );
2315
-
2316
- return [legacyCandidate];
2317
- }
2318
-
2319
- const completedCandidate = candidate
2320
- ? Object.assign(new TaskCandidateEntity(), candidate, {
2321
- claimedAt: candidate.claimedAt ?? decidedAt,
2322
- decidedAt,
2323
- status: TaskCandidateStatusEnum.COMPLETED,
2324
- })
2325
- : null;
2326
- const decisionPolicy = readDecisionPolicySnapshot(task);
2327
- const shouldCancelOpenCandidates =
2328
- action !== TaskDecisionActionEnum.APPROVED ||
2329
- decisionPolicy.type === 'SINGLE' ||
2330
- decisionPolicy.type === 'PARALLEL_ANY';
2331
- const nextCandidates = candidates.map((currentCandidate) => {
2332
- if (currentCandidate.id === completedCandidate?.id) {
2333
- return completedCandidate;
2334
- }
2335
-
2336
- if (
2337
- shouldCancelOpenCandidates &&
2338
- (currentCandidate.status === TaskCandidateStatusEnum.PENDING ||
2339
- currentCandidate.status === TaskCandidateStatusEnum.CLAIMED)
2340
- ) {
2341
- return Object.assign(new TaskCandidateEntity(), currentCandidate, {
2342
- decidedAt,
2343
- status: TaskCandidateStatusEnum.CANCELLED,
2344
- });
2345
- }
2346
-
2347
- return currentCandidate;
2348
- });
2349
-
2350
- if (nextCandidates.length > 0) {
2351
- await taskCandidateRepository.save(nextCandidates);
2352
- }
2353
-
2354
- return nextCandidates;
2355
- }
2356
-
2357
- private async transferTask(
2358
- manager: EntityManager,
2359
- instance: ApprovalInstanceEntity,
2360
- task: TaskEntity,
2361
- decision: TaskDecisionEntity,
2362
- signature: { readonly id: string; readonly signedPayloadHash: string },
2363
- transferToMemberId: string | null,
2364
- decisionComment: string | null,
2365
- decidedAt: Date,
2366
- actorCandidate: TaskCandidateEntity | null,
2367
- ): Promise<void> {
2368
- if (!transferToMemberId) {
2369
- throw new BadRequestException('Transfer target member is required');
2370
- }
2371
-
2372
- const taskRepository = manager.getRepository(TaskEntity);
2373
- const taskCandidateRepository = manager.getRepository(TaskCandidateEntity);
2374
- const activityRepository = manager.getRepository(ActivityLogEntity);
2375
- const transferringMemberId = task.assigneeMemberId;
2376
-
2377
- if (!transferringMemberId) {
2378
- throw new ConflictException('Task must be claimed before transfer');
2379
- }
2380
-
2381
- const nextDelegationChain: readonly DelegationStep[] = [
2382
- ...readDelegationSteps(task.delegationChain),
2383
- {
2384
- from: transferringMemberId,
2385
- reason: 'MANUAL_TRANSFER',
2386
- ruleId: null,
2387
- to: transferToMemberId,
2388
- },
2389
- ];
2390
- const transferredTask = await taskRepository.save({
2391
- ...task,
2392
- completedAt: decidedAt,
2393
- delegationChain: nextDelegationChain,
2394
- status: TaskStatusEnum.TRANSFERRED,
2395
- });
2396
- const nextTask = await taskRepository.save(
2397
- taskRepository.create({
2398
- assigneeMemberId: transferToMemberId,
2399
- assignmentType: TaskAssignmentTypeEnum.DIRECT_MEMBER,
2400
- completedAt: null,
2401
- decisionPolicySnapshot: { type: 'SINGLE' },
2402
- delegationChain: nextDelegationChain,
2403
- instanceId: task.instanceId,
2404
- nodeId: task.nodeId,
2405
- openedAt: null,
2406
- originalAssigneeMemberId: task.originalAssigneeMemberId,
2407
- slaDueAt: task.slaDueAt,
2408
- status: TaskStatusEnum.PENDING,
2409
- tokenId: task.tokenId,
2410
- }),
2411
- );
2412
- await taskCandidateRepository.save([
2413
- ...(actorCandidate
2414
- ? [
2415
- taskCandidateRepository.create({
2416
- ...actorCandidate,
2417
- decidedAt,
2418
- status: TaskCandidateStatusEnum.TRANSFERRED,
2419
- }),
2420
- ]
2421
- : []),
2422
- taskCandidateRepository.create({
2423
- claimedAt: null,
2424
- createdAt: decidedAt,
2425
- decidedAt: null,
2426
- delegationChain: nextDelegationChain,
2427
- memberId: transferToMemberId,
2428
- originalMemberId: task.originalAssigneeMemberId ?? transferToMemberId,
2429
- sourceType: 'DIRECT',
2430
- status: TaskCandidateStatusEnum.PENDING,
2431
- taskId: nextTask.id,
2432
- }),
2433
- ]);
2434
-
2435
- await activityRepository.save([
2436
- activityRepository.create({
2437
- actorMemberId: task.assigneeMemberId,
2438
- eventType: ActivityLogEventTypeEnum.TASK_DECIDED,
2439
- instanceId: instance.id,
2440
- nodeId: task.nodeId,
2441
- payload: {
2442
- action: TaskDecisionActionEnum.TRANSFERRED,
2443
- comment: decisionComment,
2444
- decisionId: decision.id,
2445
- signatureId: signature.id,
2446
- signedPayloadHash: signature.signedPayloadHash,
2447
- transferToMemberId,
2448
- },
2449
- taskId: transferredTask.id,
2450
- }),
2451
- activityRepository.create({
2452
- actorMemberId: transferringMemberId,
2453
- eventType: ActivityLogEventTypeEnum.TASK_CREATED,
2454
- instanceId: instance.id,
2455
- nodeId: task.nodeId,
2456
- payload: {
2457
- assigneeMemberId: transferToMemberId,
2458
- delegationChain: nextDelegationChain,
2459
- originalAssigneeMemberId: task.originalAssigneeMemberId,
2460
- tokenId: task.tokenId,
2461
- transferredFromTaskId: task.id,
2462
- },
2463
- taskId: nextTask.id,
2464
- }),
2465
- ]);
2466
- const node = readWorkflowNodeOrThrow(
2467
- instance.workflowSnapshot,
2468
- task.nodeId,
2469
- );
2470
-
2471
- if (node.type === 'userTask') {
2472
- await this.notificationService.createTaskAssignedNotification({
2473
- instance,
2474
- manager,
2475
- node,
2476
- task: nextTask,
2477
- transferred: true,
2478
- });
2479
- }
2480
- }
2481
-
2482
- private async resolveRuntimeTaskCandidates(
2483
- manager: EntityManager,
2484
- instance: ApprovalInstanceEntity,
2485
- node: UserTaskNode,
2486
- ): Promise<readonly RuntimeTaskCandidate[]> {
2487
- const resolvedCandidates = await this.resolveApproverResolver(
2488
- manager,
2489
- instance,
2490
- node.data.approverResolver,
2491
- `簽核節點「${node.data.label}」`,
2492
- );
2493
- const context = {
2494
- formData: instance.formData,
2495
- initiatorMemberId: instance.initiatorMemberId,
2496
- initiatorMetadataSnapshot: instance.initiatorMetadataSnapshot,
2497
- instanceId: instance.id,
2498
- nodeId: node.id,
2499
- state: instance.state,
2500
- templateId: instance.templateId,
2501
- templateVersionId: instance.templateVersionId,
2502
- title: instance.title,
2503
- };
2504
- const delegatedCandidates = await Promise.all(
2505
- resolvedCandidates.map(
2506
- async (candidate): Promise<RuntimeTaskCandidate> => {
2507
- const delegationResolution =
2508
- await this.delegationService.resolveAssignee(
2509
- candidate.memberId,
2510
- context,
2511
- );
2512
-
2513
- return {
2514
- delegationChain: delegationResolution.delegationChain,
2515
- memberId: delegationResolution.finalAssigneeMemberId,
2516
- originalMemberId: candidate.memberId,
2517
- sourceType: candidate.sourceType,
2518
- };
2519
- },
2520
- ),
2521
- );
2522
- const uniqueCandidates = uniqueRuntimeCandidates(delegatedCandidates);
2523
-
2524
- if (uniqueCandidates.length === 0) {
2525
- throw new ConflictException(
2526
- `簽核節點「${node.data.label}」 did not resolve to any member id`,
2527
- );
2528
- }
2529
-
2530
- return uniqueCandidates;
2531
- }
2532
-
2533
- private async resolveApproverResolver(
2534
- manager: EntityManager,
2535
- instance: ApprovalInstanceEntity,
2536
- resolver: ApproverResolver,
2537
- label: string,
2538
- ): Promise<readonly ResolvedApproverCandidate[]> {
2539
- if (resolver.type === 'DIRECT') {
2540
- return readMemberIdsFromValue(resolver.memberIds, label).map(
2541
- (memberId) => ({ memberId, sourceType: resolver.type }),
2542
- );
2543
- }
2544
-
2545
- if (resolver.type === 'DYNAMIC_FORM') {
2546
- return readMemberIdsFromValue(
2547
- readValueAtPath(instance.formData, resolver.formPath),
2548
- `${label}.formPath`,
2549
- ).map((memberId) => ({ memberId, sourceType: resolver.type }));
2550
- }
2551
-
2552
- if (resolver.type === 'EXPRESSION') {
2553
- return readMemberIdsFromValue(
2554
- this.conditionService.evaluateValue(
2555
- resolver.expression,
2556
- buildWorkflowExpressionContext(instance),
2557
- `${label}.expression`,
2558
- ),
2559
- `${label}.expression`,
2560
- ).map((memberId) => ({ memberId, sourceType: resolver.type }));
2561
- }
2562
-
2563
- if (resolver.type === 'POSITION') {
2564
- return this.resolvePositionCandidates(
2565
- manager,
2566
- resolver.positionId,
2567
- label,
2568
- ).then((memberIds) =>
2569
- memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })),
2570
- );
2571
- }
2572
-
2573
- if (resolver.type === 'ORG_UNIT_MEMBER') {
2574
- return this.resolveOrgUnitMemberCandidates(
2575
- manager,
2576
- resolver.orgUnitId,
2577
- Boolean(resolver.includeDescendants),
2578
- label,
2579
- ).then((memberIds) =>
2580
- memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })),
2581
- );
2582
- }
2583
-
2584
- if (resolver.type === 'ORG_UNIT_POSITION') {
2585
- return this.resolveOrgUnitPositionCandidates(
2586
- manager,
2587
- resolver.orgUnitId,
2588
- resolver.positionId,
2589
- Boolean(resolver.includeDescendants),
2590
- label,
2591
- ).then((memberIds) =>
2592
- memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })),
2593
- );
2594
- }
2595
-
2596
- if (resolver.type === 'ORG_UNIT_MANAGER') {
2597
- return this.resolveOrgUnitManagerCandidates(
2598
- manager,
2599
- instance,
2600
- resolver.orgUnitId,
2601
- resolver.fallback,
2602
- label,
2603
- ).then((memberIds) =>
2604
- memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })),
2605
- );
2606
- }
2607
-
2608
- return this.resolveInitiatorManagerCandidates(
2609
- manager,
2610
- instance,
2611
- resolver.levelsUp,
2612
- resolver.fallback,
2613
- label,
2614
- ).then((memberIds) =>
2615
- memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })),
2616
- );
2617
- }
2618
-
2619
- private async resolvePositionCandidates(
2620
- manager: EntityManager,
2621
- positionId: string,
2622
- label: string,
2623
- ): Promise<readonly string[]> {
2624
- const today = toDateOnlyString(new Date());
2625
- const memberships = await manager.getRepository(MembershipEntity).find({
2626
- where: { positionId },
2627
- });
2628
- const activeMemberships = memberships
2629
- .filter(
2630
- (membership) =>
2631
- membership.effectiveFrom <= today &&
2632
- (!membership.effectiveTo || membership.effectiveTo >= today),
2633
- )
2634
- .sort((first, second) => {
2635
- if (first.isPrimary !== second.isPrimary) {
2636
- return first.isPrimary ? -1 : 1;
2637
- }
2638
-
2639
- return second.effectiveFrom.localeCompare(first.effectiveFrom);
2640
- });
2641
-
2642
- if (!activeMemberships.length) {
2643
- throw new ConflictException(
2644
- `${label} position ${positionId} has no active membership`,
2645
- );
2646
- }
2647
-
2648
- return uniqueTexts(
2649
- activeMemberships.map((membership) => membership.memberId),
2650
- );
2651
- }
2652
-
2653
- private async resolveInitiatorManagerCandidates(
2654
- manager: EntityManager,
2655
- instance: ApprovalInstanceEntity,
2656
- levelsUp: number,
2657
- fallback: ApproverResolverFallback | undefined,
2658
- label: string,
2659
- ): Promise<readonly string[]> {
2660
- const normalizedLevelsUp = Math.max(Math.trunc(levelsUp), 1);
2661
- const resolvedManagerMemberIds =
2662
- await this.resolveManagerMemberIdsFromOrganization(
2663
- manager,
2664
- instance.initiatorMemberId,
2665
- normalizedLevelsUp,
2666
- );
2667
-
2668
- if (resolvedManagerMemberIds.length) {
2669
- return resolvedManagerMemberIds;
2670
- }
2671
-
2672
- return [
2673
- this.resolveFallbackAssignee(
2674
- instance,
2675
- fallback,
2676
- label,
2677
- `找不到發起人的第 ${normalizedLevelsUp} 層主管`,
2678
- ),
2679
- ];
2680
- }
2681
-
2682
- private resolveFallbackAssignee(
2683
- instance: ApprovalInstanceEntity,
2684
- fallback: ApproverResolverFallback | undefined,
2685
- label: string,
2686
- failureReason: string,
2687
- ): string {
2688
- if (!fallback || fallback.type === 'NONE') {
2689
- throw new ConflictException(
2690
- `${label} 無法建立待簽任務:${failureReason},且未設定改派固定人。請聯絡流程管理員調整主管規則或模板 fallback。`,
2691
- );
2692
- }
2693
-
2694
- if (!fallback.memberId.trim()) {
2695
- throw new ConflictException(
2696
- `${label} 無法建立待簽任務:改派固定人未設定。`,
2697
- );
2698
- }
2699
-
2700
- if (
2701
- fallback.memberId === instance.initiatorMemberId &&
2702
- !fallback.allowInitiatorSelfApproval
2703
- ) {
2704
- throw new ConflictException(
2705
- `${label} 無法建立待簽任務:改派固定人是申請人本人,且此節點未允許申請人自簽。`,
2706
- );
2707
- }
2708
-
2709
- return fallback.memberId;
2710
- }
2711
-
2712
- private async resolveManagerMemberIdsFromOrganization(
2713
- manager: EntityManager,
2714
- memberId: string,
2715
- levelsUp: number,
2716
- ): Promise<readonly string[]> {
2717
- return this.resolveManagerMemberIdsAtLevel(
2718
- manager,
2719
- memberId,
2720
- toDateOnlyString(new Date()),
2721
- levelsUp,
2722
- );
2723
- }
2724
-
2725
- private async resolveManagerMemberIdsAtLevel(
2726
- manager: EntityManager,
2727
- memberId: string,
2728
- date: string,
2729
- remainingLevels: number,
2730
- ): Promise<readonly string[]> {
2731
- const directManagerMemberIds = await this.resolveDirectManagerMemberIds(
2732
- manager,
2733
- memberId,
2734
- date,
2735
- );
2736
-
2737
- if (remainingLevels <= 1 || directManagerMemberIds.length === 0) {
2738
- return directManagerMemberIds;
2739
- }
2740
-
2741
- const nextLevelGroups = await Promise.all(
2742
- directManagerMemberIds.map((managerMemberId) =>
2743
- this.resolveManagerMemberIdsAtLevel(
2744
- manager,
2745
- managerMemberId,
2746
- date,
2747
- remainingLevels - 1,
2748
- ),
2749
- ),
2750
- );
2751
-
2752
- return uniqueTexts(nextLevelGroups.flat());
2753
- }
2754
-
2755
- private async resolveDirectManagerMemberIds(
2756
- manager: EntityManager,
2757
- memberId: string,
2758
- date: string,
2759
- ): Promise<readonly string[]> {
2760
- const memberships = await manager.getRepository(MembershipEntity).find({
2761
- where: { memberId },
2762
- });
2763
- const activeMemberships = memberships.filter((membership) =>
2764
- isDateRangeActive(membership, date),
2765
- );
2766
- const directOrgUnitIds = activeMemberships.map(
2767
- (membership) => membership.orgUnitId,
2768
- );
2769
- const positionIds = activeMemberships
2770
- .map((membership) => membership.positionId)
2771
- .filter((positionId): positionId is string => Boolean(positionId));
2772
- const orgUnitIds = await this.readOrgUnitAndAncestorIds(
2773
- manager,
2774
- directOrgUnitIds,
2775
- );
2776
- const candidatePairs = [
2777
- { scopeId: memberId, scopeType: ManagerResolutionScopeTypeEnum.MEMBER },
2778
- ...orgUnitIds.map((scopeId) => ({
2779
- scopeId,
2780
- scopeType: ManagerResolutionScopeTypeEnum.ORG_UNIT,
2781
- })),
2782
- ...positionIds.map((scopeId) => ({
2783
- scopeId,
2784
- scopeType: ManagerResolutionScopeTypeEnum.POSITION,
2785
- })),
2786
- ];
2787
-
2788
- return this.resolveManagerResolutionCandidates(
2789
- manager,
2790
- candidatePairs,
2791
- date,
2792
- );
2793
- }
2794
-
2795
- private async resolveOrgUnitManagerCandidates(
2796
- manager: EntityManager,
2797
- instance: ApprovalInstanceEntity,
2798
- orgUnitId: string,
2799
- fallback: ApproverResolverFallback | undefined,
2800
- label: string,
2801
- ): Promise<readonly string[]> {
2802
- const date = toDateOnlyString(new Date());
2803
- const orgUnitIds = await this.readOrgUnitAndAncestorIds(manager, [
2804
- orgUnitId,
2805
- ]);
2806
- const managerMemberIds = await this.resolveManagerResolutionCandidates(
2807
- manager,
2808
- orgUnitIds.map((scopeId) => ({
2809
- scopeId,
2810
- scopeType: ManagerResolutionScopeTypeEnum.ORG_UNIT,
2811
- })),
2812
- date,
2813
- );
2814
-
2815
- if (!managerMemberIds.length) {
2816
- return [
2817
- this.resolveFallbackAssignee(
2818
- instance,
2819
- fallback,
2820
- label,
2821
- `找不到指定組織的主管規則`,
2822
- ),
2823
- ];
2824
- }
2825
-
2826
- return managerMemberIds;
2827
- }
2828
-
2829
- private async readOrgUnitAndAncestorIds(
2830
- manager: EntityManager,
2831
- orgUnitIds: readonly string[],
2832
- ): Promise<readonly string[]> {
2833
- if (!orgUnitIds.length) {
2834
- return [];
2835
- }
2836
-
2837
- const orgUnitRepository = manager.getRepository(OrgUnitEntity);
2838
- const directOrgUnits = await orgUnitRepository.find({
2839
- where: { deletedAt: IsNull(), id: In([...orgUnitIds]) },
2840
- });
2841
- const ancestorGroups = await Promise.all(
2842
- directOrgUnits.map((orgUnit) =>
2843
- orgUnitRepository
2844
- .createQueryBuilder('orgUnit')
2845
- .where('orgUnit.deleted_at IS NULL')
2846
- .andWhere('orgUnit.path @> :path', { path: orgUnit.path })
2847
- .getMany(),
2848
- ),
2849
- );
2850
- const byId = new Map(
2851
- [...directOrgUnits, ...ancestorGroups.flat()].map(
2852
- (orgUnit): readonly [string, OrgUnitEntity] => [orgUnit.id, orgUnit],
2853
- ),
2854
- );
2855
-
2856
- return [...byId.keys()];
2857
- }
2858
-
2859
- private async resolveManagerResolutionCandidates(
2860
- manager: EntityManager,
2861
- candidatePairs: readonly {
2862
- readonly scopeId: string;
2863
- readonly scopeType: ManagerResolutionScopeTypeEnum;
2864
- }[],
2865
- date: string,
2866
- ): Promise<readonly string[]> {
2867
- const scopeIds = candidatePairs.map((pair) => pair.scopeId);
2868
-
2869
- if (!scopeIds.length) {
2870
- return [];
2871
- }
2872
-
2873
- const resolutions = await manager
2874
- .getRepository(ManagerResolutionEntity)
2875
- .find({
2876
- where: {
2877
- scopeId: In([...scopeIds]),
2878
- scopeType: In([
2879
- ManagerResolutionScopeTypeEnum.MEMBER,
2880
- ManagerResolutionScopeTypeEnum.ORG_UNIT,
2881
- ManagerResolutionScopeTypeEnum.POSITION,
2882
- ]),
2883
- },
2884
- });
2885
- const active = resolutions
2886
- .filter((resolution) =>
2887
- candidatePairs.some(
2888
- (pair) =>
2889
- pair.scopeId === resolution.scopeId &&
2890
- pair.scopeType === resolution.scopeType,
2891
- ),
2892
- )
2893
- .filter((resolution) => isDateRangeActive(resolution, date))
2894
- .sort(compareManagerResolution);
2895
-
2896
- return uniqueTexts(active.map((resolution) => resolution.managerMemberId));
2897
- }
2898
-
2899
- private async resolveOrgUnitMemberCandidates(
2900
- manager: EntityManager,
2901
- orgUnitId: string,
2902
- includeDescendants: boolean,
2903
- label: string,
2904
- ): Promise<readonly string[]> {
2905
- const orgUnitIds = await this.readOrgUnitScopeIds(
2906
- manager,
2907
- orgUnitId,
2908
- includeDescendants,
2909
- );
2910
- const today = toDateOnlyString(new Date());
2911
- const memberships = await manager.getRepository(MembershipEntity).find({
2912
- where: { orgUnitId: In([...orgUnitIds]) },
2913
- });
2914
- const memberIds = memberships
2915
- .filter((membership) => isDateRangeActive(membership, today))
2916
- .sort(compareMembership)
2917
- .map((membership) => membership.memberId);
2918
-
2919
- if (memberIds.length === 0) {
2920
- throw new ConflictException(
2921
- `${label} orgUnit ${orgUnitId} has no active member`,
2922
- );
2923
- }
2924
-
2925
- return uniqueTexts(memberIds);
2926
- }
2927
-
2928
- private async resolveOrgUnitPositionCandidates(
2929
- manager: EntityManager,
2930
- orgUnitId: string,
2931
- positionId: string,
2932
- includeDescendants: boolean,
2933
- label: string,
2934
- ): Promise<readonly string[]> {
2935
- const orgUnitIds = await this.readOrgUnitScopeIds(
2936
- manager,
2937
- orgUnitId,
2938
- includeDescendants,
2939
- );
2940
- const today = toDateOnlyString(new Date());
2941
- const memberships = await manager.getRepository(MembershipEntity).find({
2942
- where: { orgUnitId: In([...orgUnitIds]), positionId },
2943
- });
2944
- const memberIds = memberships
2945
- .filter((membership) => isDateRangeActive(membership, today))
2946
- .sort(compareMembership)
2947
- .map((membership) => membership.memberId);
2948
-
2949
- if (memberIds.length === 0) {
2950
- throw new ConflictException(
2951
- `${label} orgUnit ${orgUnitId} and position ${positionId} has no active membership`,
2952
- );
2953
- }
2954
-
2955
- return uniqueTexts(memberIds);
2956
- }
2957
-
2958
- private async readOrgUnitScopeIds(
2959
- manager: EntityManager,
2960
- orgUnitId: string,
2961
- includeDescendants: boolean,
2962
- ): Promise<readonly string[]> {
2963
- const orgUnitRepository = manager.getRepository(OrgUnitEntity);
2964
- const orgUnit = await orgUnitRepository.findOne({
2965
- where: { deletedAt: IsNull(), id: orgUnitId },
2966
- });
2967
-
2968
- if (!orgUnit) {
2969
- throw new ConflictException(`Org unit ${orgUnitId} was not found`);
2970
- }
2971
-
2972
- if (!includeDescendants) {
2973
- return [orgUnit.id];
2974
- }
2975
-
2976
- const descendants = await orgUnitRepository
2977
- .createQueryBuilder('orgUnit')
2978
- .where('orgUnit.deleted_at IS NULL')
2979
- .andWhere('orgUnit.path <@ :path', { path: orgUnit.path })
2980
- .getMany();
2981
-
2982
- return uniqueTexts([orgUnit.id, ...descendants.map((unit) => unit.id)]);
2983
- }
2984
-
2985
- private async processExclusiveGateway(
2986
- manager: EntityManager,
2987
- instance: ApprovalInstanceEntity,
2988
- token: WorkflowTokenEntity,
2989
- node: Extract<WorkflowNode, { readonly type: 'exclusiveGateway' }>,
2990
- ): Promise<void> {
2991
- if (node.data.direction !== 'split') {
2992
- await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
2993
-
2994
- return;
2995
- }
2996
-
2997
- const edge = (
2998
- await this.readAvailableOutgoingEdges(manager, instance, node)
2999
- )[0];
3000
-
3001
- await this.advanceTokenAlongEdge(manager, instance, token, node, edge);
3002
- }
3003
-
3004
- private async executeServiceTask(
3005
- manager: EntityManager,
3006
- instance: ApprovalInstanceEntity,
3007
- token: WorkflowTokenEntity,
3008
- node: ServiceTaskNode,
3009
- ): Promise<void> {
3010
- const activityRepository = manager.getRepository(ActivityLogEntity);
3011
- const action = node.data.action;
3012
-
3013
- if (action.type === 'WEBHOOK') {
3014
- const payload =
3015
- typeof action.payload === 'string' && action.payload.trim()
3016
- ? this.conditionService.evaluateValue(
3017
- action.payload,
3018
- buildWorkflowExpressionContext(instance),
3019
- `workflow.nodes.${node.id}.data.action.payload`,
3020
- )
3021
- : {
3022
- instance: buildWorkflowExpressionContext(instance).instance,
3023
- triggeredAt: new Date().toISOString(),
3024
- };
3025
- const result = await executeWebhookServiceAction(
3026
- this.serviceTaskDispatcher,
3027
- action,
3028
- payload,
3029
- );
3030
-
3031
- await activityRepository.save(
3032
- activityRepository.create({
3033
- actorMemberId: null,
3034
- eventType: result.ok
3035
- ? ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED
3036
- : ActivityLogEventTypeEnum.SERVICE_TASK_FAILED,
3037
- instanceId: instance.id,
3038
- nodeId: node.id,
3039
- payload: {
3040
- action: 'WEBHOOK',
3041
- error: result.error,
3042
- ok: result.ok,
3043
- status: result.status,
3044
- tokenId: token.id,
3045
- url: action.url,
3046
- },
3047
- taskId: null,
3048
- }),
3049
- );
3050
- await this.advanceTokenToOutgoingNodes(manager, instance, token, node);
3051
-
3052
- return;
3053
- }
3054
-
3055
- if (action.type === 'SET_FORM_FIELD') {
3056
- const value = this.conditionService.evaluateValue(
3057
- action.value,
3058
- buildWorkflowExpressionContext(instance),
3059
- `workflow.nodes.${node.id}.data.action.value`,
3060
- );
3061
- const updatedFormData = writeValueAtPath(
3062
- instance.formData,
3063
- action.fieldPath,
3064
- value,
3065
- );
3066
- const updatedInstance = await manager
3067
- .getRepository(ApprovalInstanceEntity)
3068
- .save({
3069
- ...instance,
3070
- formData: updatedFormData,
3071
- });
3072
-
3073
- Object.assign(instance, updatedInstance);
3074
- await activityRepository.save(
3075
- activityRepository.create({
3076
- actorMemberId: null,
3077
- eventType: ActivityLogEventTypeEnum.SERVICE_TASK_EXECUTED,
3078
- instanceId: instance.id,
3079
- nodeId: node.id,
3080
- payload: {
3081
- action: 'SET_FORM_FIELD',
3082
- fieldPath: action.fieldPath,
3083
- tokenId: token.id,
3084
- },
3085
- taskId: null,
3086
- }),
3087
- );
3088
- await this.advanceTokenToOutgoingNodes(
3089
- manager,
3090
- updatedInstance,
3091
- token,
3092
- node,
3093
- );
3094
-
3095
- return;
3096
- }
3097
-
3098
- const recipients = await this.resolveApproverResolver(
3099
- manager,
3100
- instance,
3101
- action.recipients,
3102
- `知會節點「${node.data.label}」`,
3103
- );
3104
- const recipientMemberIds = uniqueTexts(
3105
- recipients.map((recipient) => recipient.memberId),
3106
- );
3107
-
3108
- await this.notificationService.createServiceTaskNotifications({
3109
- instance,
3110
- manager,
3111
- node,
3112
- recipientMemberIds,
3113
- });
3114
- await manager.getRepository(WorkflowTokenEntity).save({
3115
- ...token,
3116
- consumedAt: new Date(),
3117
- status: WorkflowTokenStatusEnum.CONSUMED,
3118
- });
3119
- await activityRepository.save(
3120
- activityRepository.create({
3121
- actorMemberId: null,
3122
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
3123
- instanceId: instance.id,
3124
- nodeId: node.id,
3125
- payload: {
3126
- action: 'NOTIFY',
3127
- recipientMemberIds,
3128
- tokenId: token.id,
3129
- },
3130
- taskId: null,
3131
- }),
3132
- );
3133
- }
3134
-
3135
- private async rejectInstance(
3136
- manager: EntityManager,
3137
- instance: ApprovalInstanceEntity,
3138
- task: TaskEntity,
3139
- rejectedAt: Date,
3140
- ): Promise<void> {
3141
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
3142
- const instanceRepository = manager.getRepository(ApprovalInstanceEntity);
3143
- const activeTokens = await tokenRepository.find({
3144
- where: { instanceId: instance.id },
3145
- });
3146
- const consumedTokens = activeTokens
3147
- .filter(
3148
- (token) =>
3149
- token.status === WorkflowTokenStatusEnum.ACTIVE ||
3150
- token.status === WorkflowTokenStatusEnum.WAITING,
3151
- )
3152
- .map(
3153
- (token): WorkflowTokenEntity => ({
3154
- ...token,
3155
- consumedAt: rejectedAt,
3156
- status: WorkflowTokenStatusEnum.CONSUMED,
3157
- }),
3158
- );
3159
-
3160
- if (consumedTokens.length) {
3161
- await tokenRepository.save(consumedTokens);
3162
- }
3163
-
3164
- await instanceRepository.save({
3165
- ...instance,
3166
- completedAt: rejectedAt,
3167
- state: ApprovalInstanceStateEnum.REJECTED,
3168
- });
3169
- await manager.getRepository(ActivityLogEntity).save(
3170
- manager.getRepository(ActivityLogEntity).create({
3171
- actorMemberId: task.assigneeMemberId,
3172
- eventType: ActivityLogEventTypeEnum.TOKEN_ADVANCED,
3173
- instanceId: instance.id,
3174
- nodeId: task.nodeId,
3175
- payload: {
3176
- action: 'REJECTED',
3177
- taskId: task.id,
3178
- },
3179
- taskId: task.id,
3180
- }),
3181
- );
3182
- }
3183
-
3184
- private async returnInstanceToNode(
3185
- manager: EntityManager,
3186
- instance: ApprovalInstanceEntity,
3187
- task: TaskEntity,
3188
- returnToNodeId: string | null,
3189
- returnedAt: Date,
3190
- ): Promise<void> {
3191
- if (!returnToNodeId) {
3192
- throw new ConflictException('Return target node is required');
3193
- }
3194
-
3195
- const targetNode = readWorkflowNodeOrThrow(
3196
- instance.workflowSnapshot,
3197
- returnToNodeId,
3198
- );
3199
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
3200
- const instanceRepository = manager.getRepository(ApprovalInstanceEntity);
3201
- const activityRepository = manager.getRepository(ActivityLogEntity);
3202
-
3203
- await this.consumeOpenRuntimeState(
3204
- manager,
3205
- instance,
3206
- returnedAt,
3207
- TaskStatusEnum.CANCELLED,
3208
- );
3209
-
3210
- const returnedInstance = await instanceRepository.save({
3211
- ...instance,
3212
- completedAt: null,
3213
- state:
3214
- targetNode.type === 'startEvent'
3215
- ? ApprovalInstanceStateEnum.RETURNED
3216
- : ApprovalInstanceStateEnum.RUNNING,
3217
- });
3218
- const resubmitStrategy = readReturnResubmitStrategy(task, instance);
3219
- const returnToken = await tokenRepository.save(
3220
- tokenRepository.create({
3221
- consumedAt: null,
3222
- currentNodeId: targetNode.id,
3223
- instanceId: instance.id,
3224
- parentTokenId: null,
3225
- status: WorkflowTokenStatusEnum.ACTIVE,
3226
- }),
3227
- );
3228
-
3229
- await activityRepository.save([
3230
- activityRepository.create({
3231
- actorMemberId: task.assigneeMemberId,
3232
- eventType: ActivityLogEventTypeEnum.INSTANCE_RETURNED,
3233
- instanceId: instance.id,
3234
- nodeId: task.nodeId,
3235
- payload: {
3236
- resubmitStrategy,
3237
- returnedFromNodeId: task.nodeId,
3238
- returnToNodeId: targetNode.id,
3239
- taskId: task.id,
3240
- },
3241
- taskId: task.id,
3242
- }),
3243
- activityRepository.create({
3244
- actorMemberId: null,
3245
- eventType: ActivityLogEventTypeEnum.TOKEN_CREATED,
3246
- instanceId: instance.id,
3247
- nodeId: targetNode.id,
3248
- payload: {
3249
- returnedFromNodeId: task.nodeId,
3250
- tokenId: returnToken.id,
3251
- },
3252
- taskId: null,
3253
- }),
3254
- ]);
3255
-
3256
- if (returnedInstance.state === ApprovalInstanceStateEnum.RUNNING) {
3257
- await this.processRunningInstance(manager, returnedInstance);
3258
- }
3259
- }
3260
-
3261
- private async readLatestReturnedInstanceContext(
3262
- manager: EntityManager,
3263
- instanceId: string,
3264
- ): Promise<ReturnedInstanceContext | null> {
3265
- const activity = await manager.getRepository(ActivityLogEntity).findOne({
3266
- order: { createdAt: 'DESC' },
3267
- where: {
3268
- eventType: ActivityLogEventTypeEnum.INSTANCE_RETURNED,
3269
- instanceId,
3270
- },
3271
- });
3272
-
3273
- if (!activity) {
3274
- return null;
3275
- }
3276
-
3277
- const returnToNodeId = readStringPayloadValue(
3278
- activity.payload,
3279
- 'returnToNodeId',
3280
- );
3281
- const returnedFromNodeId =
3282
- readStringPayloadValue(activity.payload, 'returnedFromNodeId') ??
3283
- activity.nodeId;
3284
-
3285
- if (!returnToNodeId || !returnedFromNodeId) {
3286
- return null;
3287
- }
3288
-
3289
- return {
3290
- resubmitStrategy: readReturnResubmitStrategyFromPayload(activity.payload),
3291
- returnedFromNodeId,
3292
- returnToNodeId,
3293
- };
3294
- }
3295
-
3296
- private async resumeReturnedInstanceFromReturnPoint(
3297
- manager: EntityManager,
3298
- instance: ApprovalInstanceEntity,
3299
- context: ReturnedInstanceContext,
3300
- resubmittedAt: Date,
3301
- ): Promise<ApprovalInstanceEntity> {
3302
- const targetNode = readWorkflowNodeOrThrow(
3303
- instance.workflowSnapshot,
3304
- context.returnToNodeId,
3305
- );
3306
-
3307
- if (targetNode.type !== 'startEvent') {
3308
- return instance;
3309
- }
3310
-
3311
- readWorkflowNodeOrThrow(
3312
- instance.workflowSnapshot,
3313
- context.returnedFromNodeId,
3314
- );
3315
-
3316
- await this.consumeOpenRuntimeState(
3317
- manager,
3318
- instance,
3319
- resubmittedAt,
3320
- TaskStatusEnum.CANCELLED,
3321
- );
3322
-
3323
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
3324
- const returnPointToken = await tokenRepository.save(
3325
- tokenRepository.create({
3326
- consumedAt: null,
3327
- currentNodeId: context.returnedFromNodeId,
3328
- instanceId: instance.id,
3329
- parentTokenId: null,
3330
- status: WorkflowTokenStatusEnum.ACTIVE,
3331
- }),
3332
- );
3333
-
3334
- await manager.getRepository(ActivityLogEntity).save(
3335
- manager.getRepository(ActivityLogEntity).create({
3336
- actorMemberId: null,
3337
- eventType: ActivityLogEventTypeEnum.TOKEN_CREATED,
3338
- instanceId: instance.id,
3339
- nodeId: context.returnedFromNodeId,
3340
- payload: {
3341
- resubmitStrategy: context.resubmitStrategy,
3342
- resumedFromNodeId: context.returnToNodeId,
3343
- tokenId: returnPointToken.id,
3344
- },
3345
- taskId: null,
3346
- }),
3347
- );
3348
-
3349
- return instance;
3350
- }
3351
-
3352
- private async consumeOpenRuntimeState(
3353
- manager: EntityManager,
3354
- instance: ApprovalInstanceEntity,
3355
- consumedAt: Date,
3356
- taskStatus: TaskStatusEnum,
3357
- ): Promise<void> {
3358
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
3359
- const taskRepository = manager.getRepository(TaskEntity);
3360
- const tokens = await tokenRepository.find({
3361
- where: { instanceId: instance.id },
3362
- });
3363
- const openTokens = tokens.filter(
3364
- (token) =>
3365
- token.status === WorkflowTokenStatusEnum.ACTIVE ||
3366
- token.status === WorkflowTokenStatusEnum.WAITING,
3367
- );
3368
- const tasks = await taskRepository.find({
3369
- where: { instanceId: instance.id },
3370
- });
3371
- const openTasks = tasks.filter(
3372
- (task) =>
3373
- task.status === TaskStatusEnum.PENDING ||
3374
- task.status === TaskStatusEnum.IN_PROGRESS,
3375
- );
3376
-
3377
- if (openTokens.length > 0) {
3378
- await tokenRepository.save(
3379
- openTokens.map(
3380
- (token): WorkflowTokenEntity => ({
3381
- ...token,
3382
- consumedAt,
3383
- status: WorkflowTokenStatusEnum.CONSUMED,
3384
- }),
3385
- ),
3386
- );
3387
- }
3388
-
3389
- if (openTasks.length > 0) {
3390
- await taskRepository.save(
3391
- openTasks.map(
3392
- (openTask): TaskEntity =>
3393
- Object.assign(new TaskEntity(), openTask, {
3394
- completedAt: consumedAt,
3395
- status: taskStatus,
3396
- }),
3397
- ),
3398
- );
3399
- await this.closeTaskCandidates(
3400
- manager,
3401
- openTasks.map((task) => task.id),
3402
- readCandidateStatusForClosedTask(taskStatus),
3403
- );
3404
- }
3405
- }
3406
-
3407
- private async consumeJoinedTokens(
3408
- manager: EntityManager,
3409
- tokens: readonly WorkflowTokenEntity[],
3410
- ): Promise<void> {
3411
- if (tokens.length === 0) {
3412
- return;
3413
- }
3414
-
3415
- const consumedAt = new Date();
3416
-
3417
- await manager.getRepository(WorkflowTokenEntity).save(
3418
- tokens.map(
3419
- (token): WorkflowTokenEntity => ({
3420
- ...token,
3421
- consumedAt,
3422
- status: WorkflowTokenStatusEnum.CONSUMED,
3423
- }),
3424
- ),
3425
- );
3426
- }
3427
-
3428
- private async cancelAlternativeTokensForOrTrigger(
3429
- manager: EntityManager,
3430
- instance: ApprovalInstanceEntity,
3431
- token: WorkflowTokenEntity,
3432
- targetNodeId: string,
3433
- ): Promise<void> {
3434
- const tokenRepository = manager.getRepository(WorkflowTokenEntity);
3435
- const taskRepository = manager.getRepository(TaskEntity);
3436
- const consumedAt = new Date();
3437
- const tokens = await tokenRepository.find({
3438
- where: { instanceId: instance.id },
3439
- });
3440
- const alternativeTokens = tokens.filter(
3441
- (candidate) =>
3442
- candidate.id !== token.id &&
3443
- (candidate.status === WorkflowTokenStatusEnum.ACTIVE ||
3444
- candidate.status === WorkflowTokenStatusEnum.WAITING) &&
3445
- hasPathToNode(
3446
- instance.workflowSnapshot,
3447
- candidate.currentNodeId,
3448
- targetNodeId,
3449
- ),
3450
- );
3451
-
3452
- if (alternativeTokens.length === 0) {
3453
- return;
3454
- }
3455
-
3456
- const alternativeTokenIds = alternativeTokens.map(
3457
- (candidate) => candidate.id,
3458
- );
3459
- const cancellableTasks = (
3460
- await taskRepository.find({
3461
- where: { instanceId: instance.id },
3462
- })
3463
- ).filter(
3464
- (task) =>
3465
- alternativeTokenIds.includes(task.tokenId) &&
3466
- (task.status === TaskStatusEnum.PENDING ||
3467
- task.status === TaskStatusEnum.IN_PROGRESS),
3468
- );
3469
-
3470
- await tokenRepository.save(
3471
- alternativeTokens.map(
3472
- (candidate): WorkflowTokenEntity => ({
3473
- ...candidate,
3474
- consumedAt,
3475
- status: WorkflowTokenStatusEnum.CONSUMED,
3476
- }),
3477
- ),
3478
- );
3479
-
3480
- if (cancellableTasks.length) {
3481
- await taskRepository.save(
3482
- cancellableTasks.map(
3483
- (task): TaskEntity =>
3484
- Object.assign(new TaskEntity(), task, {
3485
- completedAt: consumedAt,
3486
- status: TaskStatusEnum.CANCELLED,
3487
- }),
3488
- ),
3489
- );
3490
- await this.closeTaskCandidates(
3491
- manager,
3492
- cancellableTasks.map((task) => task.id),
3493
- TaskCandidateStatusEnum.SUPERSEDED,
3494
- );
3495
- }
3496
- }
3497
-
3498
- private async closeTaskCandidates(
3499
- manager: EntityManager,
3500
- taskIds: readonly string[],
3501
- status: TaskCandidateStatusEnum,
3502
- ): Promise<void> {
3503
- if (taskIds.length === 0) {
3504
- return;
3505
- }
3506
-
3507
- const candidateRepository = manager.getRepository(TaskCandidateEntity);
3508
- const candidates = (
3509
- await candidateRepository.find({ where: { taskId: In([...taskIds]) } })
3510
- ).filter(
3511
- (candidate) =>
3512
- candidate.status === TaskCandidateStatusEnum.PENDING ||
3513
- candidate.status === TaskCandidateStatusEnum.CLAIMED,
3514
- );
3515
-
3516
- if (candidates.length === 0) {
3517
- return;
3518
- }
3519
-
3520
- await candidateRepository.save(
3521
- candidates.map(
3522
- (candidate): TaskCandidateEntity =>
3523
- Object.assign(new TaskCandidateEntity(), candidate, { status }),
3524
- ),
3525
- );
3526
- }
3527
-
3528
- private async getTemplateOrThrow(
3529
- templateId: string,
3530
- ): Promise<ApprovalTemplateEntity> {
3531
- const template = await this.approvalTemplateRepository.findOne({
3532
- where: { deletedAt: IsNull(), id: templateId },
3533
- });
3534
-
3535
- if (!template) {
3536
- throw new NotFoundException(
3537
- `Approval template ${templateId} was not found`,
3538
- );
3539
- }
3540
-
3541
- return template;
3542
- }
3543
-
3544
- private async getPublishedTemplateVersionOrThrow(
3545
- versionId: string,
3546
- ): Promise<ApprovalTemplateVersionEntity> {
3547
- const version = await this.approvalTemplateVersionRepository.findOne({
3548
- where: { id: versionId },
3549
- });
3550
-
3551
- if (!version) {
3552
- throw new NotFoundException(
3553
- `Approval template version ${versionId} was not found`,
3554
- );
3555
- }
3556
-
3557
- if (version.status !== ApprovalTemplateVersionStatusEnum.PUBLISHED) {
3558
- throw new ConflictException(
3559
- 'Approval template current version must be published',
3560
- );
3561
- }
3562
-
3563
- return version;
3564
- }
3565
-
3566
- private async getPublishedFormVersionOrThrow(
3567
- versionId: string | null,
3568
- ): Promise<FormDefinitionVersionEntity> {
3569
- if (!versionId) {
3570
- throw new ConflictException(
3571
- 'Approval template version does not bind a form version',
3572
- );
3573
- }
3574
-
3575
- const version = await this.formDefinitionVersionRepository.findOne({
3576
- where: { id: versionId },
3577
- });
3578
-
3579
- if (!version) {
3580
- throw new NotFoundException(
3581
- `Form definition version ${versionId} was not found`,
3582
- );
3583
- }
3584
-
3585
- if (version.status !== FormDefinitionVersionStatusEnum.PUBLISHED) {
3586
- throw new ConflictException(
3587
- 'Approval template must bind a published form version',
3588
- );
3589
- }
3590
-
3591
- return version;
3592
- }
3593
- }
3594
-
3595
- function parseJsonObject(
3596
- value: string,
3597
- label: string,
3598
- ): Readonly<Record<string, unknown>> {
3599
- try {
3600
- const parsedValue = JSON.parse(value) as unknown;
3601
-
3602
- if (isRecord(parsedValue)) {
3603
- return parsedValue;
3604
- }
3605
-
3606
- throw new BadRequestException(`${label} must be a JSON object`);
3607
- } catch (error: unknown) {
3608
- if (error instanceof BadRequestException) {
3609
- throw error;
3610
- }
3611
-
3612
- throw new BadRequestException(
3613
- error instanceof Error ? error.message : `${label} is invalid JSON`,
3614
- );
3615
- }
3616
- }
3617
-
3618
- function readDefaultInitiatorMetadataSnapshot(
3619
- memberId: string,
3620
- ): Readonly<Record<string, unknown>> {
3621
- return {
3622
- customFields: {},
3623
- memberId,
3624
- };
3625
- }
3626
-
3627
- function parseWorkflowDefinition(value: string): WorkflowDefinition {
3628
- try {
3629
- const parsedValue = JSON.parse(value) as unknown;
3630
-
3631
- if (
3632
- isRecord(parsedValue) &&
3633
- Array.isArray(parsedValue.nodes) &&
3634
- Array.isArray(parsedValue.edges) &&
3635
- isRecord(parsedValue.meta)
3636
- ) {
3637
- return parsedValue as unknown as WorkflowDefinition;
3638
- }
3639
-
3640
- throw new BadRequestException(
3641
- 'workflowDefinitionJson must be a workflow definition object',
3642
- );
3643
- } catch (error: unknown) {
3644
- if (error instanceof BadRequestException) {
3645
- throw error;
3646
- }
3647
-
3648
- throw new BadRequestException(
3649
- error instanceof Error
3650
- ? error.message
3651
- : 'workflowDefinitionJson is invalid JSON',
3652
- );
3653
- }
3654
- }
3655
-
3656
- function buildInitiatorPolicyContext(
3657
- initiatorMetadataSnapshot: Readonly<Record<string, unknown>>,
3658
- ): Readonly<Record<string, unknown>> {
3659
- return {
3660
- env: {
3661
- now: new Date().toISOString(),
3662
- },
3663
- initiator: initiatorMetadataSnapshot,
3664
- subject: initiatorMetadataSnapshot,
3665
- };
3666
- }
3667
-
3668
- function buildWorkflowExpressionContext(
3669
- instance: ApprovalInstanceEntity,
3670
- ): Readonly<Record<string, unknown>> {
3671
- return {
3672
- env: {
3673
- now: new Date().toISOString(),
3674
- },
3675
- form: instance.formData,
3676
- formData: instance.formData,
3677
- initiator: {
3678
- ...instance.initiatorMetadataSnapshot,
3679
- memberId: instance.initiatorMemberId,
3680
- },
3681
- instance: {
3682
- id: instance.id,
3683
- state: instance.state,
3684
- templateId: instance.templateId,
3685
- templateVersionId: instance.templateVersionId,
3686
- title: instance.title,
3687
- },
3688
- };
3689
- }
3690
-
3691
- function readDelegationSteps(
3692
- value: readonly Readonly<Record<string, unknown>>[],
3693
- ): readonly DelegationStep[] {
3694
- return value
3695
- .map((step): DelegationStep | null => {
3696
- const from = readStringValue(step.from);
3697
- const to = readStringValue(step.to);
3698
- const reason = readStringValue(step.reason);
3699
-
3700
- if (!from || !to || !reason) {
3701
- return null;
3702
- }
3703
-
3704
- return {
3705
- from,
3706
- reason,
3707
- ruleId: readStringValue(step.ruleId),
3708
- to,
3709
- };
3710
- })
3711
- .filter((step): step is DelegationStep => step !== null);
3712
- }
3713
-
3714
- function readStringValue(value: unknown): string | null {
3715
- return typeof value === 'string' && value.trim() ? value : null;
3716
- }
3717
-
3718
- function buildDryRunExpressionContext(
3719
- input: DryRunSimulationInput,
3720
- lastDecision: Readonly<Record<string, unknown>> | null,
3721
- ): Readonly<Record<string, unknown>> {
3722
- return {
3723
- env: {
3724
- now: new Date().toISOString(),
3725
- },
3726
- form: input.formData,
3727
- formData: input.formData,
3728
- initiator: {
3729
- ...input.initiatorMetadataSnapshot,
3730
- memberId: input.initiatorMemberId,
3731
- },
3732
- instance: {
3733
- id: SYSTEM_DRY_RUN_INSTANCE_ID,
3734
- state: ApprovalInstanceStateEnum.RUNNING,
3735
- title: 'Dry Run',
3736
- },
3737
- lastDecision,
3738
- };
3739
- }
3740
-
3741
- function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
3742
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3743
- }
3744
-
3745
- function readWorkflowNodeOrThrow(
3746
- workflow: WorkflowDefinition,
3747
- nodeId: string,
3748
- ): WorkflowNode {
3749
- const node = workflow.nodes.find((candidate) => candidate.id === nodeId);
3750
-
3751
- if (!node) {
3752
- throw new ConflictException(`Workflow node ${nodeId} was not found`);
3753
- }
3754
-
3755
- return node;
3756
- }
3757
-
3758
- function readOutgoingEdgesOrThrow(
3759
- workflow: WorkflowDefinition,
3760
- nodeId: string,
3761
- ): readonly WorkflowEdge[] {
3762
- const edges = workflow.edges.filter((edge) => edge.source === nodeId);
3763
-
3764
- if (edges.length === 0) {
3765
- throw new ConflictException(`Workflow node ${nodeId} has no outgoing edge`);
3766
- }
3767
-
3768
- return edges;
3769
- }
3770
-
3771
- function readIncomingEdges(
3772
- workflow: WorkflowDefinition,
3773
- nodeId: string,
3774
- ): readonly WorkflowEdge[] {
3775
- return workflow.edges.filter((edge) => edge.target === nodeId);
3776
- }
3777
-
3778
- function hasPathToNode(
3779
- workflow: WorkflowDefinition,
3780
- sourceNodeId: string,
3781
- targetNodeId: string,
3782
- ): boolean {
3783
- return visitPathToNode(workflow, sourceNodeId, targetNodeId, new Set());
3784
- }
3785
-
3786
- function visitPathToNode(
3787
- workflow: WorkflowDefinition,
3788
- sourceNodeId: string,
3789
- targetNodeId: string,
3790
- visitedNodeIds: ReadonlySet<string>,
3791
- ): boolean {
3792
- if (sourceNodeId === targetNodeId) {
3793
- return true;
3794
- }
3795
-
3796
- if (visitedNodeIds.has(sourceNodeId)) {
3797
- return false;
3798
- }
3799
-
3800
- const nextVisitedNodeIds = new Set([...visitedNodeIds, sourceNodeId]);
3801
-
3802
- return workflow.edges
3803
- .filter((edge) => edge.source === sourceNodeId)
3804
- .some((edge) =>
3805
- visitPathToNode(workflow, edge.target, targetNodeId, nextVisitedNodeIds),
3806
- );
3807
- }
3808
-
3809
- function readReturnTargetNodeId(
3810
- workflow: WorkflowDefinition,
3811
- currentNodeId: string,
3812
- requestedNodeId?: string | null,
3813
- ): string {
3814
- const currentNode = readWorkflowNodeOrThrow(workflow, currentNodeId);
3815
-
3816
- if (currentNode.type !== 'userTask') {
3817
- throw new ConflictException(
3818
- `Workflow node ${currentNodeId} does not support return decisions`,
3819
- );
3820
- }
3821
-
3822
- if (!currentNode.data.returnBehavior.allowReturn) {
3823
- throw new ConflictException(
3824
- `User task ${currentNodeId} does not allow return decisions`,
3825
- );
3826
- }
3827
-
3828
- const trimmedRequestedNodeId = requestedNodeId?.trim() || null;
3829
- const defaultTargetNodeId = readDefaultReturnTargetNodeId(
3830
- workflow,
3831
- currentNode,
3832
- );
3833
-
3834
- if (currentNode.data.returnBehavior.allowedTargets === 'ANY') {
3835
- if (!trimmedRequestedNodeId) {
3836
- throw new BadRequestException('returnToNodeId is required');
3837
- }
3838
-
3839
- readWorkflowNodeOrThrow(workflow, trimmedRequestedNodeId);
3840
-
3841
- return trimmedRequestedNodeId;
3842
- }
3843
-
3844
- if (
3845
- trimmedRequestedNodeId &&
3846
- trimmedRequestedNodeId !== defaultTargetNodeId
3847
- ) {
3848
- throw new ConflictException(
3849
- `User task ${currentNodeId} can only return to ${defaultTargetNodeId}`,
3850
- );
3851
- }
3852
-
3853
- return defaultTargetNodeId;
3854
- }
3855
-
3856
- function readDefaultReturnTargetNodeId(
3857
- workflow: WorkflowDefinition,
3858
- currentNode: UserTaskNode,
3859
- ): string {
3860
- if (currentNode.data.returnBehavior.allowedTargets === 'INITIATOR') {
3861
- const startNode = workflow.nodes.find((node) => node.type === 'startEvent');
3862
-
3863
- if (!startNode) {
3864
- throw new ConflictException('Workflow does not include a startEvent');
3865
- }
3866
-
3867
- return startNode.id;
3868
- }
3869
-
3870
- const previousNodeId = readIncomingEdges(workflow, currentNode.id)[0]?.source;
3871
-
3872
- if (!previousNodeId) {
3873
- throw new ConflictException(
3874
- `User task ${currentNode.id} does not have a previous node to return to`,
3875
- );
3876
- }
3877
-
3878
- return previousNodeId;
3879
- }
3880
-
3881
- function readReturnResubmitStrategy(
3882
- task: TaskEntity,
3883
- instance: ApprovalInstanceEntity,
3884
- ): ReturnResubmitStrategy {
3885
- const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
3886
-
3887
- if (node.type !== 'userTask') {
3888
- return 'RESTART';
3889
- }
3890
-
3891
- return node.data.returnBehavior.resubmitStrategy ?? 'RESTART';
3892
- }
3893
-
3894
- function readReturnResubmitStrategyFromPayload(
3895
- payload: Readonly<Record<string, unknown>>,
3896
- ): ReturnResubmitStrategy {
3897
- return payload.resubmitStrategy === 'FROM_RETURN_POINT'
3898
- ? 'FROM_RETURN_POINT'
3899
- : 'RESTART';
3900
- }
3901
-
3902
- function readStringPayloadValue(
3903
- payload: Readonly<Record<string, unknown>>,
3904
- key: string,
3905
- ): string | null {
3906
- const value = payload[key];
3907
-
3908
- return typeof value === 'string' && value.trim() ? value.trim() : null;
3909
- }
3910
-
3911
- function readNodeEntryCondition(node: WorkflowNode): string | null {
3912
- if (node.type === 'userTask' || node.type === 'serviceTask') {
3913
- return node.data.entryCondition?.trim() || null;
3914
- }
3915
-
3916
- return null;
3917
- }
3918
-
3919
- function edgeHasCondition(edge: WorkflowEdge): boolean {
3920
- return Boolean(
3921
- edge.data.condition?.trim() ||
3922
- (edge.data.conditionFieldKey && edge.data.conditionOperator),
3923
- );
3924
- }
3925
-
3926
- function readMemberIdFromValue(value: unknown, label: string): string {
3927
- return readMemberIdsFromValue(value, label)[0] ?? '';
3928
- }
3929
-
3930
- function readMemberIdsFromValue(
3931
- value: unknown,
3932
- label: string,
3933
- ): readonly string[] {
3934
- if (typeof value === 'string' && value.trim()) {
3935
- return [value.trim()];
3936
- }
3937
-
3938
- if (Array.isArray(value)) {
3939
- const memberIds = value
3940
- .filter(
3941
- (candidate): candidate is string =>
3942
- typeof candidate === 'string' && Boolean(candidate.trim()),
3943
- )
3944
- .map((candidate) => candidate.trim());
3945
-
3946
- if (memberIds.length > 0) {
3947
- return uniqueTexts(memberIds);
3948
- }
3949
- }
3950
-
3951
- throw new ConflictException(`${label} did not resolve to a member id`);
3952
- }
3953
-
3954
- function uniqueTexts(values: readonly string[]): readonly string[] {
3955
- return [...new Set(values.filter((value) => value.trim()))];
3956
- }
3957
-
3958
- function uniqueRuntimeCandidates(
3959
- candidates: readonly RuntimeTaskCandidate[],
3960
- ): readonly RuntimeTaskCandidate[] {
3961
- const seen = new Set<string>();
3962
-
3963
- return candidates.filter((candidate) => {
3964
- if (seen.has(candidate.memberId)) {
3965
- return false;
3966
- }
3967
-
3968
- seen.add(candidate.memberId);
3969
-
3970
- return true;
3971
- });
3972
- }
3973
-
3974
- function uniqueTasksById(tasks: readonly TaskEntity[]): readonly TaskEntity[] {
3975
- const seen = new Set<string>();
3976
-
3977
- return tasks.filter((task) => {
3978
- if (seen.has(task.id)) {
3979
- return false;
3980
- }
3981
-
3982
- seen.add(task.id);
3983
-
3984
- return true;
3985
- });
3986
- }
3987
-
3988
- function normalizeText(value: string | null | undefined): string | null {
3989
- return typeof value === 'string' && value.trim() ? value.trim() : null;
3990
- }
3991
-
3992
- function readPageOffset(
3993
- page: number | null | undefined,
3994
- pageSize: number | null | undefined,
3995
- ): number {
3996
- const normalizedPageSize = normalizePageSize(pageSize);
3997
-
3998
- return (normalizePage(page) - 1) * normalizedPageSize;
3999
- }
4000
-
4001
- function shouldPaginateList(options: ListApprovalInstancesOptions): boolean {
4002
- return (
4003
- typeof options.page !== 'undefined' ||
4004
- typeof options.pageSize !== 'undefined'
4005
- );
4006
- }
4007
-
4008
- function normalizePage(value: number | null | undefined): number {
4009
- return Number.isInteger(value) && Number(value) > 0 ? Number(value) : 1;
4010
- }
4011
-
4012
- function normalizePageSize(value: number | null | undefined): number {
4013
- if (!Number.isInteger(value) || Number(value) <= 0) {
4014
- return 20;
4015
- }
4016
-
4017
- return Math.min(Number(value), 100);
4018
- }
4019
-
4020
- function applyApprovalInstanceSearchFilter(
4021
- queryBuilder: SelectQueryBuilder<ApprovalInstanceEntity>,
4022
- searchTextValue: string | null | undefined,
4023
- ): void {
4024
- const searchText = normalizeText(searchTextValue)?.toLocaleLowerCase();
4025
-
4026
- if (!searchText) {
4027
- return;
4028
- }
4029
-
4030
- queryBuilder.andWhere(
4031
- [
4032
- '(',
4033
- 'LOWER("approvalInstance"."title") LIKE :approvalInstanceSearchText',
4034
- 'OR LOWER("approvalInstance"."initiator_member_id") LIKE :approvalInstanceSearchText',
4035
- 'OR LOWER(CAST("approvalInstance"."id" AS text)) LIKE :approvalInstanceSearchText',
4036
- ')',
4037
- ].join(' '),
4038
- {
4039
- approvalInstanceSearchText: `%${escapeLikePattern(searchText)}%`,
4040
- },
4041
- );
4042
- }
4043
-
4044
- function escapeLikePattern(value: string): string {
4045
- return value
4046
- .replace(/\\/gu, '\\\\')
4047
- .replace(/%/gu, '\\%')
4048
- .replace(/_/gu, '\\_');
4049
- }
4050
-
4051
- function isInstanceWithinRange(
4052
- instance: ApprovalInstanceEntity,
4053
- options: WorkflowDashboardSummaryOptions,
4054
- ): boolean {
4055
- const fromTime = options.from?.getTime() ?? null;
4056
- const toTime = options.to?.getTime() ?? null;
4057
- const candidateTime = (
4058
- instance.completedAt ??
4059
- instance.startedAt ??
4060
- instance.createdAt
4061
- ).getTime();
4062
-
4063
- return (
4064
- (fromTime === null || candidateTime >= fromTime) &&
4065
- (toTime === null || candidateTime <= toTime)
4066
- );
4067
- }
4068
-
4069
- function readDecisionPolicySnapshot(task: TaskEntity): DecisionPolicy {
4070
- const value = task.decisionPolicySnapshot;
4071
-
4072
- if (
4073
- isRecord(value) &&
4074
- typeof value.type === 'string' &&
4075
- (value.type === 'SINGLE' ||
4076
- value.type === 'SEQUENTIAL' ||
4077
- value.type === 'PARALLEL_ALL' ||
4078
- value.type === 'PARALLEL_ANY')
4079
- ) {
4080
- return { type: value.type };
4081
- }
4082
-
4083
- if (
4084
- isRecord(value) &&
4085
- value.type === 'QUORUM' &&
4086
- typeof value.threshold === 'number' &&
4087
- (value.thresholdType === 'COUNT' || value.thresholdType === 'PERCENTAGE')
4088
- ) {
4089
- return {
4090
- threshold: value.threshold,
4091
- thresholdType: value.thresholdType,
4092
- type: 'QUORUM',
4093
- };
4094
- }
4095
-
4096
- return { type: 'SINGLE' };
4097
- }
4098
-
4099
- function shouldCompleteTaskAfterDecision({
4100
- action,
4101
- candidates,
4102
- decisionPolicy,
4103
- }: {
4104
- readonly action: TaskDecisionActionEnum;
4105
- readonly candidates: readonly TaskCandidateEntity[];
4106
- readonly decisionPolicy: DecisionPolicy;
4107
- }): boolean {
4108
- if (action !== TaskDecisionActionEnum.APPROVED) {
4109
- return true;
4110
- }
4111
-
4112
- if (
4113
- decisionPolicy.type === 'SINGLE' ||
4114
- decisionPolicy.type === 'PARALLEL_ANY'
4115
- ) {
4116
- return true;
4117
- }
4118
-
4119
- const completedCount = candidates.filter(
4120
- (candidate) => candidate.status === TaskCandidateStatusEnum.COMPLETED,
4121
- ).length;
4122
- const totalCount = Math.max(candidates.length, 1);
4123
-
4124
- if (
4125
- decisionPolicy.type === 'PARALLEL_ALL' ||
4126
- decisionPolicy.type === 'SEQUENTIAL'
4127
- ) {
4128
- return completedCount >= totalCount;
4129
- }
4130
-
4131
- const threshold =
4132
- decisionPolicy.thresholdType === 'PERCENTAGE'
4133
- ? Math.ceil((totalCount * decisionPolicy.threshold) / 100)
4134
- : decisionPolicy.threshold;
4135
-
4136
- return completedCount >= Math.max(threshold, 1);
4137
- }
4138
-
4139
- function readValueAtPath(
4140
- value: Readonly<Record<string, unknown>>,
4141
- path: string,
4142
- ): unknown {
4143
- return path
4144
- .split('.')
4145
- .filter((segment) => segment.trim())
4146
- .reduce<unknown>((currentValue, segment) => {
4147
- if (!isRecord(currentValue)) {
4148
- return undefined;
4149
- }
4150
-
4151
- return currentValue[segment];
4152
- }, value);
4153
- }
4154
-
4155
- function writeValueAtPath(
4156
- value: Readonly<Record<string, unknown>>,
4157
- path: string,
4158
- nextValue: unknown,
4159
- ): Readonly<Record<string, unknown>> {
4160
- const segments = normalizeFormFieldPath(path);
4161
-
4162
- if (segments.length === 0) {
4163
- return value;
4164
- }
4165
-
4166
- return writeNestedValue(value, segments, nextValue);
4167
- }
4168
-
4169
- function writeNestedValue(
4170
- value: Readonly<Record<string, unknown>>,
4171
- segments: readonly string[],
4172
- nextValue: unknown,
4173
- ): Readonly<Record<string, unknown>> {
4174
- const [currentSegment, ...remainingSegments] = segments;
4175
-
4176
- if (!currentSegment) {
4177
- return value;
4178
- }
4179
-
4180
- if (remainingSegments.length === 0) {
4181
- return { ...value, [currentSegment]: nextValue };
4182
- }
4183
-
4184
- const currentValue = value[currentSegment];
4185
- const nestedValue = isRecord(currentValue) ? currentValue : {};
4186
-
4187
- return {
4188
- ...value,
4189
- [currentSegment]: writeNestedValue(
4190
- nestedValue,
4191
- remainingSegments,
4192
- nextValue,
4193
- ),
4194
- };
4195
- }
4196
-
4197
- function normalizeFormFieldPath(path: string): readonly string[] {
4198
- const segments = path
4199
- .split('.')
4200
- .map((segment) => segment.trim())
4201
- .filter(Boolean);
4202
-
4203
- if (segments[0] === 'form' || segments[0] === 'formData') {
4204
- return segments.slice(1);
4205
- }
4206
-
4207
- return segments;
4208
- }
4209
-
4210
- async function executeWebhookServiceAction(
4211
- serviceTaskDispatcher: BPMWorkflowServiceTaskDispatcher,
4212
- action: Extract<
4213
- ServiceTaskNode['data']['action'],
4214
- { readonly type: 'WEBHOOK' }
4215
- >,
4216
- payload: unknown,
4217
- ): Promise<BPMWorkflowWebhookDispatchResult> {
4218
- try {
4219
- return await serviceTaskDispatcher.dispatchWebhook({
4220
- headers: action.headers,
4221
- payload,
4222
- url: action.url,
4223
- });
4224
- } catch (error: unknown) {
4225
- return {
4226
- error: error instanceof Error ? error.message : 'Unknown webhook error',
4227
- ok: false,
4228
- status: null,
4229
- };
4230
- }
4231
- }
4232
-
4233
- function readManagerMemberIdFromInitiatorSnapshot(
4234
- initiatorMetadataSnapshot: Readonly<Record<string, unknown>>,
4235
- levelsUp: number,
4236
- ): unknown {
4237
- const managerChain = initiatorMetadataSnapshot.managerChain;
4238
-
4239
- if (Array.isArray(managerChain)) {
4240
- return managerChain[Math.max(levelsUp - 1, 0)];
4241
- }
4242
-
4243
- const managerMemberIds = initiatorMetadataSnapshot.managerMemberIds;
4244
-
4245
- if (Array.isArray(managerMemberIds)) {
4246
- return managerMemberIds[Math.max(levelsUp - 1, 0)];
4247
- }
4248
-
4249
- const customFields = initiatorMetadataSnapshot.customFields;
4250
-
4251
- if (isRecord(customFields)) {
4252
- return customFields.managerMemberId;
4253
- }
4254
-
4255
- return initiatorMetadataSnapshot.managerMemberId;
4256
- }
4257
-
4258
- function isDateRangeActive(
4259
- value: Pick<
4260
- MembershipEntity | ManagerResolutionEntity,
4261
- 'effectiveFrom' | 'effectiveTo'
4262
- >,
4263
- date: string,
4264
- ): boolean {
4265
- return (
4266
- value.effectiveFrom <= date &&
4267
- (!value.effectiveTo || value.effectiveTo >= date)
4268
- );
4269
- }
4270
-
4271
- function compareManagerResolution(
4272
- left: ManagerResolutionEntity,
4273
- right: ManagerResolutionEntity,
4274
- ): number {
4275
- const priorityDiff = right.priority - left.priority;
4276
-
4277
- if (priorityDiff !== 0) {
4278
- return priorityDiff;
4279
- }
4280
-
4281
- return (
4282
- readManagerResolutionScopeRank(right.scopeType) -
4283
- readManagerResolutionScopeRank(left.scopeType) ||
4284
- right.effectiveFrom.localeCompare(left.effectiveFrom)
4285
- );
4286
- }
4287
-
4288
- function readManagerResolutionScopeRank(
4289
- scopeType: ManagerResolutionScopeTypeEnum,
4290
- ): number {
4291
- const ranks: Readonly<Record<ManagerResolutionScopeTypeEnum, number>> = {
4292
- [ManagerResolutionScopeTypeEnum.MEMBER]: 3,
4293
- [ManagerResolutionScopeTypeEnum.ORG_UNIT]: 2,
4294
- [ManagerResolutionScopeTypeEnum.POSITION]: 1,
4295
- };
4296
-
4297
- return ranks[scopeType];
4298
- }
4299
-
4300
- function compareMembership(
4301
- left: MembershipEntity,
4302
- right: MembershipEntity,
4303
- ): number {
4304
- if (left.isPrimary !== right.isPrimary) {
4305
- return left.isPrimary ? -1 : 1;
4306
- }
4307
-
4308
- return right.effectiveFrom.localeCompare(left.effectiveFrom);
4309
- }
4310
-
4311
- function toDateOnlyString(value: Date): string {
4312
- return value.toISOString().slice(0, 10);
4313
- }
4314
-
4315
- function canReadAllWorkflows(scope: WorkflowReadScope): boolean {
4316
- return (
4317
- scope.roles.includes('BPM_ADMIN') ||
4318
- scope.permissions.some((permission) =>
4319
- WORKFLOW_READ_ALL_PERMISSIONS.has(permission),
4320
- )
4321
- );
4322
- }
4323
-
4324
- function isTaskRelatedToMember(task: TaskEntity, memberId: string): boolean {
4325
- return (
4326
- task.assigneeMemberId === memberId ||
4327
- task.originalAssigneeMemberId === memberId
4328
- );
4329
- }
4330
-
4331
- function readCandidateStatusForClosedTask(
4332
- taskStatus: TaskStatusEnum,
4333
- ): TaskCandidateStatusEnum {
4334
- if (taskStatus === TaskStatusEnum.TRANSFERRED) {
4335
- return TaskCandidateStatusEnum.TRANSFERRED;
4336
- }
4337
-
4338
- if (taskStatus === TaskStatusEnum.COMPLETED) {
4339
- return TaskCandidateStatusEnum.COMPLETED;
4340
- }
4341
-
4342
- return TaskCandidateStatusEnum.CANCELLED;
4343
- }
4344
-
4345
- function validateSubmittedFormData(
4346
- schema: FormDefinitionSchema,
4347
- formData: Readonly<Record<string, unknown>>,
4348
- ): void {
4349
- const missingFields = schema.fields.filter((field) => {
4350
- const fieldValue = formData[field.fieldKey];
4351
-
4352
- return (
4353
- isSubmittedFormFieldVisible(field, schema.fields, formData) &&
4354
- isSubmittedFormFieldRequired(field, schema.fields, formData) &&
4355
- !isSubmittedFieldValuePresent(fieldValue)
4356
- );
4357
- });
4358
-
4359
- if (missingFields.length > 0) {
4360
- throw new BadRequestException(
4361
- `Form data is missing required fields: ${missingFields
4362
- .map((field) => field.label || field.fieldKey)
4363
- .join(', ')}`,
4364
- );
4365
- }
4366
- }
4367
-
4368
- function readFormDefinitionSnapshotSchema(
4369
- snapshot: Readonly<Record<string, unknown>>,
4370
- ): FormDefinitionSchema {
4371
- const schema = snapshot.schema;
4372
-
4373
- if (isFormDefinitionSchema(schema)) {
4374
- return schema;
4375
- }
4376
-
4377
- throw new BadRequestException('Approval instance form schema is invalid');
4378
- }
4379
-
4380
- function isFormDefinitionSchema(value: unknown): value is FormDefinitionSchema {
4381
- return (
4382
- isRecord(value) && value.schemaVersion === 1 && Array.isArray(value.fields)
4383
- );
4384
- }
4385
-
4386
- function isSubmittedFormFieldVisible(
4387
- field: FormFieldDefinition,
4388
- fields: readonly FormFieldDefinition[],
4389
- values: Readonly<Record<string, unknown>>,
4390
- ): boolean {
4391
- return field.visibleWhen
4392
- ? evaluateFormConditionExpression(field.visibleWhen, fields, values, true)
4393
- : true;
4394
- }
4395
-
4396
- function isSubmittedFormFieldRequired(
4397
- field: FormFieldDefinition,
4398
- fields: readonly FormFieldDefinition[],
4399
- values: Readonly<Record<string, unknown>>,
4400
- ): boolean {
4401
- return (
4402
- field.required ||
4403
- Boolean(
4404
- field.requiredWhen
4405
- ? evaluateFormConditionExpression(
4406
- field.requiredWhen,
4407
- fields,
4408
- values,
4409
- false,
4410
- )
4411
- : false,
4412
- )
4413
- );
4414
- }
4415
-
4416
- function evaluateFormConditionExpression(
4417
- expression: string,
4418
- fields: readonly FormFieldDefinition[],
4419
- values: Readonly<Record<string, unknown>>,
4420
- fallback: boolean,
4421
- ): boolean {
4422
- const rule = parseFormConditionRule(expression);
4423
-
4424
- if (!rule) {
4425
- return fallback;
4426
- }
4427
-
4428
- const field = fields.find(
4429
- (candidate) => candidate.fieldKey === rule.fieldKey,
4430
- );
4431
-
4432
- if (!field) {
4433
- return fallback;
4434
- }
4435
-
4436
- return evaluateFormConditionRule(rule, field, values[field.fieldKey]);
4437
- }
4438
-
4439
- function parseFormConditionRule(expression: string): {
4440
- readonly fieldKey: string;
4441
- readonly operator: FormConditionOperator;
4442
- readonly value: string;
4443
- } | null {
4444
- const match = expression
4445
- .trim()
4446
- .match(/^form\.([A-Za-z][A-Za-z0-9_]*)\s*(==|!=|>=|<=|>|<)\s*(.+)$/u);
4447
-
4448
- if (!match) {
4449
- return null;
4450
- }
4451
-
4452
- const operator = readFormConditionOperatorFromSymbol(match[2]);
4453
-
4454
- if (!operator) {
4455
- return null;
4456
- }
4457
-
4458
- return {
4459
- fieldKey: match[1],
4460
- operator,
4461
- value: parseFormConditionLiteral(match[3]),
4462
- };
4463
- }
4464
-
4465
- function evaluateFormConditionRule(
4466
- rule: {
4467
- readonly operator: FormConditionOperator;
4468
- readonly value: string;
4469
- },
4470
- field: FormFieldDefinition,
4471
- value: unknown,
4472
- ): boolean {
4473
- if (Array.isArray(value)) {
4474
- return evaluateArrayFormCondition(value, rule);
4475
- }
4476
-
4477
- if (field.type === 'boolean') {
4478
- return compareFormConditionValues(
4479
- value === true ? 'true' : 'false',
4480
- rule.value,
4481
- rule.operator,
4482
- );
4483
- }
4484
-
4485
- if (isNumberFormField(field)) {
4486
- return compareNumericFormCondition(value, rule);
4487
- }
4488
-
4489
- return compareFormConditionValues(
4490
- typeof value === 'undefined' || value === null ? '' : String(value),
4491
- rule.value,
4492
- rule.operator,
4493
- );
4494
- }
4495
-
4496
- function evaluateArrayFormCondition(
4497
- value: readonly unknown[],
4498
- rule: {
4499
- readonly operator: FormConditionOperator;
4500
- readonly value: string;
4501
- },
4502
- ): boolean {
4503
- const stringValues = value.filter(
4504
- (entry): entry is string => typeof entry === 'string',
4505
- );
4506
-
4507
- if (rule.operator === 'equals') {
4508
- return stringValues.includes(rule.value);
4509
- }
4510
-
4511
- if (rule.operator === 'notEquals') {
4512
- return !stringValues.includes(rule.value);
4513
- }
4514
-
4515
- return false;
4516
- }
4517
-
4518
- function compareNumericFormCondition(
4519
- value: unknown,
4520
- rule: {
4521
- readonly operator: FormConditionOperator;
4522
- readonly value: string;
4523
- },
4524
- ): boolean {
4525
- const actualValue = typeof value === 'number' ? value : Number(value);
4526
- const expectedValue = Number(rule.value);
4527
-
4528
- if (!Number.isFinite(actualValue) || !Number.isFinite(expectedValue)) {
4529
- return false;
4530
- }
4531
-
4532
- return compareFormConditionValues(actualValue, expectedValue, rule.operator);
4533
- }
4534
-
4535
- function compareFormConditionValues(
4536
- actualValue: number | string,
4537
- expectedValue: number | string,
4538
- operator: FormConditionOperator,
4539
- ): boolean {
4540
- if (operator === 'equals') {
4541
- return actualValue === expectedValue;
4542
- }
4543
-
4544
- if (operator === 'notEquals') {
4545
- return actualValue !== expectedValue;
4546
- }
4547
-
4548
- if (operator === 'greaterThan') {
4549
- return actualValue > expectedValue;
4550
- }
4551
-
4552
- if (operator === 'greaterThanOrEqual') {
4553
- return actualValue >= expectedValue;
4554
- }
4555
-
4556
- if (operator === 'lessThan') {
4557
- return actualValue < expectedValue;
4558
- }
4559
-
4560
- return actualValue <= expectedValue;
4561
- }
4562
-
4563
- function readFormConditionOperatorFromSymbol(
4564
- symbol: string | undefined,
4565
- ): FormConditionOperator | null {
4566
- const operators: Readonly<Record<string, FormConditionOperator>> = {
4567
- '!=': 'notEquals',
4568
- '<': 'lessThan',
4569
- '<=': 'lessThanOrEqual',
4570
- '==': 'equals',
4571
- '>': 'greaterThan',
4572
- '>=': 'greaterThanOrEqual',
4573
- };
4574
-
4575
- return symbol ? (operators[symbol] ?? null) : null;
4576
- }
4577
-
4578
- function parseFormConditionLiteral(value: string): string {
4579
- const trimmedValue = value.trim();
4580
-
4581
- if (
4582
- (trimmedValue.startsWith('"') && trimmedValue.endsWith('"')) ||
4583
- (trimmedValue.startsWith("'") && trimmedValue.endsWith("'"))
4584
- ) {
4585
- return trimmedValue.slice(1, -1);
4586
- }
4587
-
4588
- return trimmedValue;
4589
- }
4590
-
4591
- function isSubmittedFieldValuePresent(value: unknown): boolean {
4592
- if (typeof value === 'undefined' || value === null) {
4593
- return false;
4594
- }
4595
-
4596
- if (typeof value === 'string') {
4597
- return value.trim().length > 0;
4598
- }
4599
-
4600
- if (Array.isArray(value)) {
4601
- return value.length > 0;
4602
- }
4603
-
4604
- return true;
4605
- }
4606
-
4607
- function isNumberFormField(
4608
- field: FormFieldDefinition,
4609
- ): field is NumberFieldDefinition {
4610
- return field.type === 'number' || field.type === 'money';
4611
- }
4612
-
4613
- function createDryRunStep(input: {
4614
- readonly assigneeMemberId: string | null;
4615
- readonly edge: WorkflowEdge | null;
4616
- readonly edgeMatched: boolean | null;
4617
- readonly edgeReason: string | null;
4618
- readonly entryCondition: string | null;
4619
- readonly entryConditionMet: boolean | null;
4620
- readonly message: string;
4621
- readonly node: WorkflowNode;
4622
- readonly status: string;
4623
- readonly stepIndex: number;
4624
- }): WorkflowDryRunStepObject {
4625
- return Object.assign(new WorkflowDryRunStepObject(), {
4626
- assigneeMemberId: input.assigneeMemberId,
4627
- edgeDefault: input.edge ? Boolean(input.edge.data.isDefault) : null,
4628
- edgeId: input.edge?.id ?? null,
4629
- edgeLabel: input.edge ? readDryRunEdgeLabel(input.edge) : null,
4630
- edgeMatched: input.edgeMatched,
4631
- edgeReason: input.edgeReason,
4632
- entryCondition: input.entryCondition,
4633
- entryConditionMatched: input.entryConditionMet,
4634
- id: `dry-run-step-${input.stepIndex + 1}`,
4635
- message: input.message,
4636
- nodeId: input.node.id,
4637
- nodeLabel: input.node.data.label,
4638
- nodeType: input.node.type,
4639
- status: input.status,
4640
- });
4641
- }
4642
-
4643
- function readDryRunEdgeLabel(edge: WorkflowEdge): string {
4644
- return edge.data.label?.trim() || edge.id;
4645
- }
4646
-
4647
- function readDryRunEdgeReason(edge: WorkflowEdge | null): string | null {
4648
- if (!edge) {
4649
- return null;
4650
- }
4651
-
4652
- if (edge.data.isDefault) {
4653
- return '其他條件不符合時採用預設路徑。';
4654
- }
4655
-
4656
- const expression = edge.data.condition?.trim();
4657
-
4658
- if (expression) {
4659
- return `條件成立:${expression}`;
4660
- }
4661
-
4662
- if (edge.data.conditionFieldKey && edge.data.conditionOperator) {
4663
- return `條件成立:${edge.data.conditionFieldKey} ${edge.data.conditionOperator}${
4664
- edge.data.conditionValue ? ` ${edge.data.conditionValue}` : ''
4665
- }`;
4666
- }
4667
-
4668
- return '無條件路徑。';
4669
- }
4670
-
4671
- function readDryRunStepStatus(
4672
- node: WorkflowNode,
4673
- entryConditionMet: boolean,
4674
- ): string {
4675
- if (!entryConditionMet) {
4676
- return 'SKIPPED';
4677
- }
4678
-
4679
- if (node.type === 'userTask') {
4680
- return 'WAITING';
4681
- }
4682
-
4683
- if (node.type === 'endEvent') {
4684
- return 'COMPLETED';
4685
- }
4686
-
4687
- return 'PASSED';
4688
- }
4689
-
4690
- function readDryRunStepMessage(
4691
- node: WorkflowNode,
4692
- entryConditionMet: boolean,
4693
- ): string {
4694
- if (!entryConditionMet) {
4695
- return '進入條件不符合,略過節點。';
4696
- }
4697
-
4698
- if (node.type === 'userTask') {
4699
- return '將建立待簽任務。';
4700
- }
4701
-
4702
- if (node.type === 'endEvent') {
4703
- return `流程結束:${node.data.endState ?? 'APPROVED'}`;
4704
- }
4705
-
4706
- return '節點條件通過。';
4707
- }