@rytass/bpm-core-nestjs-module 0.1.2 → 0.1.3
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.
- package/eslint.config.mjs +20 -0
- package/jest.config.cts +15 -0
- package/package.json +3 -3
- package/project.json +58 -0
- package/src/lib/attachment/attachment-options.spec.ts +45 -0
- package/src/lib/attachment/attachment-options.ts +130 -0
- package/src/lib/attachment/attachment-storage.provider.spec.ts +79 -0
- package/src/lib/attachment/attachment-storage.provider.ts +60 -0
- package/src/lib/attachment/{attachment-storage.token.d.ts → attachment-storage.token.ts} +3 -1
- package/src/lib/attachment/attachment.controller.ts +39 -0
- package/src/lib/attachment/attachment.entity.ts +59 -0
- package/src/lib/attachment/attachment.module.ts +149 -0
- package/src/lib/attachment/attachment.mutations.ts +22 -0
- package/src/lib/attachment/attachment.queries.ts +65 -0
- package/src/lib/attachment/attachment.service.spec.ts +319 -0
- package/src/lib/attachment/attachment.service.ts +477 -0
- package/src/lib/attachment/dto/upload-attachment.input.ts +60 -0
- package/src/lib/bpm/bpm-root.module.spec.ts +138 -0
- package/src/lib/bpm/bpm-root.module.ts +320 -0
- package/src/lib/bpm-auth/bpm-auth-context.extractor.ts +106 -0
- package/src/lib/bpm-auth/bpm-auth-context.ts +40 -0
- package/src/lib/bpm-auth/bpm-auth.authorization.ts +80 -0
- package/src/lib/bpm-auth/bpm-auth.decorators.ts +19 -0
- package/src/lib/bpm-auth/bpm-auth.guard.ts +31 -0
- package/src/lib/bpm-auth/bpm-auth.module.spec.ts +106 -0
- package/src/lib/bpm-auth/bpm-auth.module.ts +69 -0
- package/src/lib/bpm-auth/bpm-auth.options.ts +21 -0
- package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.ts +55 -0
- package/src/lib/common/filters/all-exceptions.filter.ts +138 -0
- package/src/lib/common/iso-duration.ts +31 -0
- package/src/lib/condition/condition.module.ts +8 -0
- package/src/lib/condition/condition.service.spec.ts +50 -0
- package/src/lib/condition/condition.service.ts +141 -0
- package/src/lib/database/data-source.ts +7 -0
- package/src/lib/database/migration-runner.ts +24 -0
- package/src/lib/database/typeorm.config.ts +217 -0
- package/src/lib/delegation/delegation-rule.entity.ts +84 -0
- package/src/lib/delegation/delegation.enums.ts +21 -0
- package/src/lib/delegation/delegation.module.ts +14 -0
- package/src/lib/delegation/delegation.mutations.ts +92 -0
- package/src/lib/delegation/delegation.queries.ts +111 -0
- package/src/lib/delegation/delegation.service.spec.ts +295 -0
- package/src/lib/delegation/delegation.service.ts +482 -0
- package/src/lib/delegation/dto/delegation-rule.input.ts +125 -0
- package/src/lib/form/dto/form-definition.input.ts +73 -0
- package/src/lib/form/form-definition-version.entity.ts +66 -0
- package/src/lib/form/form-definition.entity.ts +54 -0
- package/src/lib/form/form-schema-lint.object.ts +10 -0
- package/src/lib/form/form-schema.validator.spec.ts +180 -0
- package/src/lib/form/form-schema.validator.ts +538 -0
- package/src/lib/form/form.enums.ts +20 -0
- package/src/lib/form/form.module.ts +19 -0
- package/src/lib/form/form.mutations.ts +68 -0
- package/src/lib/form/form.queries.ts +72 -0
- package/src/lib/form/form.service.spec.ts +144 -0
- package/src/lib/form/form.service.ts +490 -0
- package/src/lib/identity/identity-options.ts +41 -0
- package/src/lib/identity/identity.module.ts +96 -0
- package/src/lib/identity/identity.queries.ts +61 -0
- package/src/lib/identity/identity.service.spec.ts +105 -0
- package/src/lib/identity/identity.service.ts +200 -0
- package/src/lib/identity/member-base.adapter.spec.ts +77 -0
- package/src/lib/identity/member-base.adapter.ts +244 -0
- package/src/lib/identity/member-metadata-cache.entity.ts +27 -0
- package/src/lib/identity/member-profile.object.ts +28 -0
- package/src/lib/identity/{member-resolver.interface.d.ts → member-resolver.interface.ts} +11 -5
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.ts +14 -0
- package/src/lib/migrations/{0000000001000-identity-organization-foundation.js → 0000000001000-identity-organization-foundation.ts} +38 -29
- package/src/lib/migrations/{0000000002000-form-builder-foundation.js → 0000000002000-form-builder-foundation.ts} +22 -21
- package/src/lib/migrations/{0000000003000-approval-template-foundation.js → 0000000003000-approval-template-foundation.ts} +24 -21
- package/src/lib/migrations/{0000000004000-workflow-engine-foundation.js → 0000000004000-workflow-engine-foundation.ts} +46 -34
- package/src/lib/migrations/{0000000005000-delegation-rules.js → 0000000005000-delegation-rules.ts} +20 -19
- package/src/lib/migrations/{0000000006000-notifications-sla.js → 0000000006000-notifications-sla.ts} +26 -23
- package/src/lib/migrations/{0000000007000-signatures-attachments.js → 0000000007000-signatures-attachments.ts} +28 -27
- package/src/lib/migrations/{0000000008000-approval-template-categories.js → 0000000008000-approval-template-categories.ts} +28 -25
- package/src/lib/migrations/{0000000009000-task-candidates.js → 0000000009000-task-candidates.ts} +26 -23
- package/src/lib/migrations/{0000000010000-notification-delivery-state.js → 0000000010000-notification-delivery-state.ts} +16 -17
- package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.ts +19 -0
- package/src/lib/migrations/0000000012000-notification-sla-idempotency.ts +22 -0
- package/src/lib/migrations/0000000013000-workflow-query-indexes.ts +113 -0
- package/src/lib/migrations/index.ts +47 -0
- package/src/lib/notification/dto/notification-preference.input.ts +46 -0
- package/src/lib/notification/notification-delivery-scheduler.service.ts +69 -0
- package/src/lib/notification/notification-delivery.service.spec.ts +315 -0
- package/src/lib/notification/notification-delivery.service.ts +339 -0
- package/src/lib/notification/notification-dispatcher.token.ts +13 -0
- package/src/lib/notification/notification-options.module.ts +67 -0
- package/src/lib/notification/notification-options.spec.ts +93 -0
- package/src/lib/notification/notification-options.ts +452 -0
- package/src/lib/notification/notification-preference.entity.ts +35 -0
- package/src/lib/notification/notification-sla-scheduler.service.ts +66 -0
- package/src/lib/notification/notification-template.spec.ts +33 -0
- package/src/lib/notification/notification-template.ts +97 -0
- package/src/lib/notification/notification.entity.ts +102 -0
- package/src/lib/notification/notification.enums.ts +45 -0
- package/src/lib/notification/notification.module.ts +37 -0
- package/src/lib/notification/notification.mutations.ts +47 -0
- package/src/lib/notification/notification.queries.ts +64 -0
- package/src/lib/notification/notification.service.spec.ts +412 -0
- package/src/lib/notification/notification.service.ts +974 -0
- package/src/lib/organization/dto/manager-resolution.input.ts +91 -0
- package/src/lib/organization/dto/membership.input.ts +77 -0
- package/src/lib/organization/dto/org-unit.input.ts +96 -0
- package/src/lib/organization/dto/position.input.ts +50 -0
- package/src/lib/organization/json-metadata.ts +15 -0
- package/src/lib/organization/manager-resolution.entity.ts +44 -0
- package/src/lib/organization/membership.entity.ts +50 -0
- package/src/lib/organization/org-unit-tree-commit-result.object.ts +8 -0
- package/src/lib/organization/org-unit.entity.ts +53 -0
- package/src/lib/organization/organization-summary.object.ts +16 -0
- package/src/lib/organization/organization.enums.ts +19 -0
- package/src/lib/organization/organization.module.ts +27 -0
- package/src/lib/organization/organization.mutations.ts +115 -0
- package/src/lib/organization/organization.queries.ts +188 -0
- package/src/lib/organization/organization.service.spec.ts +760 -0
- package/src/lib/organization/organization.service.ts +1216 -0
- package/src/lib/organization/position.entity.ts +39 -0
- package/src/lib/organization/resolved-manager.object.ts +10 -0
- package/src/lib/signature/signature-options.spec.ts +60 -0
- package/src/lib/signature/signature-options.ts +120 -0
- package/src/lib/signature/signature-verification.object.ts +16 -0
- package/src/lib/signature/signature.entity.ts +62 -0
- package/src/lib/signature/signature.module.ts +80 -0
- package/src/lib/signature/signature.queries.ts +37 -0
- package/src/lib/signature/signature.service.spec.ts +134 -0
- package/src/lib/signature/signature.service.ts +205 -0
- package/src/lib/template/approval-template-category.entity.ts +40 -0
- package/src/lib/template/approval-template-version.entity.ts +82 -0
- package/src/lib/template/approval-template.entity.ts +64 -0
- package/src/lib/template/dto/approval-template.input.ts +147 -0
- package/src/lib/template/template.enums.ts +30 -0
- package/src/lib/template/template.module.ts +25 -0
- package/src/lib/template/template.mutations.ts +106 -0
- package/src/lib/template/template.queries.ts +119 -0
- package/src/lib/template/template.service.spec.ts +476 -0
- package/src/lib/template/template.service.ts +820 -0
- package/src/lib/template/workflow-definition.validator.spec.ts +595 -0
- package/src/lib/template/workflow-definition.validator.ts +539 -0
- package/src/lib/testing/cel-js.jest.ts +46 -0
- package/src/lib/workflow-engine/activity-log.entity.ts +48 -0
- package/src/lib/workflow-engine/approval-instance-page-info.object.ts +22 -0
- package/src/lib/workflow-engine/approval-instance.entity.ts +86 -0
- package/src/lib/workflow-engine/dto/cancel-approval-instance.input.ts +18 -0
- package/src/lib/workflow-engine/dto/decide-task.input.ts +33 -0
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.ts +22 -0
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.ts +22 -0
- package/src/lib/workflow-engine/dto/submit-approval-instance.input.ts +27 -0
- package/src/lib/workflow-engine/task-candidate.entity.ts +56 -0
- package/src/lib/workflow-engine/task-decision.entity.ts +43 -0
- package/src/lib/workflow-engine/task.entity.ts +85 -0
- package/src/lib/workflow-engine/workflow-condition-evaluator.ts +168 -0
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.ts +22 -0
- package/src/lib/workflow-engine/workflow-dry-run.object.ts +58 -0
- package/src/lib/workflow-engine/workflow-engine.enums.ts +99 -0
- package/src/lib/workflow-engine/workflow-engine.module.ts +75 -0
- package/src/lib/workflow-engine/workflow-engine.mutations.ts +85 -0
- package/src/lib/workflow-engine/workflow-engine.queries.ts +229 -0
- package/src/lib/workflow-engine/workflow-engine.service.spec.ts +2689 -0
- package/src/lib/workflow-engine/workflow-engine.service.ts +4707 -0
- package/src/lib/workflow-engine/{workflow-engine.tokens.d.ts → workflow-engine.tokens.ts} +9 -3
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.ts +69 -0
- package/src/lib/workflow-engine/workflow-token.entity.ts +40 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +16 -0
- package/tsconfig.spec.json +16 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -18
- package/src/index.js.map +0 -1
- package/src/lib/attachment/attachment-options.d.ts +0 -44
- package/src/lib/attachment/attachment-options.js +0 -62
- package/src/lib/attachment/attachment-options.js.map +0 -1
- package/src/lib/attachment/attachment-storage.provider.d.ts +0 -4
- package/src/lib/attachment/attachment-storage.provider.js +0 -35
- package/src/lib/attachment/attachment-storage.provider.js.map +0 -1
- package/src/lib/attachment/attachment-storage.token.js +0 -5
- package/src/lib/attachment/attachment-storage.token.js.map +0 -1
- package/src/lib/attachment/attachment.controller.d.ts +0 -8
- package/src/lib/attachment/attachment.controller.js +0 -38
- package/src/lib/attachment/attachment.controller.js.map +0 -1
- package/src/lib/attachment/attachment.entity.d.ts +0 -14
- package/src/lib/attachment/attachment.entity.js +0 -74
- package/src/lib/attachment/attachment.entity.js.map +0 -1
- package/src/lib/attachment/attachment.module.d.ts +0 -27
- package/src/lib/attachment/attachment.module.js +0 -103
- package/src/lib/attachment/attachment.module.js.map +0 -1
- package/src/lib/attachment/attachment.mutations.d.ts +0 -8
- package/src/lib/attachment/attachment.mutations.js +0 -35
- package/src/lib/attachment/attachment.mutations.js.map +0 -1
- package/src/lib/attachment/attachment.queries.d.ts +0 -9
- package/src/lib/attachment/attachment.queries.js +0 -78
- package/src/lib/attachment/attachment.queries.js.map +0 -1
- package/src/lib/attachment/attachment.service.d.ts +0 -49
- package/src/lib/attachment/attachment.service.js +0 -276
- package/src/lib/attachment/attachment.service.js.map +0 -1
- package/src/lib/attachment/dto/upload-attachment.input.d.ts +0 -11
- package/src/lib/attachment/dto/upload-attachment.input.js +0 -68
- package/src/lib/attachment/dto/upload-attachment.input.js.map +0 -1
- package/src/lib/attachment/index.js +0 -9
- package/src/lib/attachment/index.js.map +0 -1
- package/src/lib/bpm/bpm-root.module.d.ts +0 -155
- package/src/lib/bpm/bpm-root.module.js +0 -170
- package/src/lib/bpm/bpm-root.module.js.map +0 -1
- package/src/lib/bpm/index.js +0 -5
- package/src/lib/bpm/index.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth-context.d.ts +0 -24
- package/src/lib/bpm-auth/bpm-auth-context.extractor.d.ts +0 -4
- package/src/lib/bpm-auth/bpm-auth-context.extractor.js +0 -69
- package/src/lib/bpm-auth/bpm-auth-context.extractor.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth-context.js +0 -15
- package/src/lib/bpm-auth/bpm-auth-context.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth.authorization.d.ts +0 -12
- package/src/lib/bpm-auth/bpm-auth.authorization.js +0 -67
- package/src/lib/bpm-auth/bpm-auth.authorization.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth.decorators.d.ts +0 -3
- package/src/lib/bpm-auth/bpm-auth.decorators.js +0 -11
- package/src/lib/bpm-auth/bpm-auth.decorators.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth.guard.d.ts +0 -7
- package/src/lib/bpm-auth/bpm-auth.guard.js +0 -24
- package/src/lib/bpm-auth/bpm-auth.guard.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth.module.d.ts +0 -6
- package/src/lib/bpm-auth/bpm-auth.module.js +0 -69
- package/src/lib/bpm-auth/bpm-auth.module.js.map +0 -1
- package/src/lib/bpm-auth/bpm-auth.options.d.ts +0 -11
- package/src/lib/bpm-auth/bpm-auth.options.js +0 -3
- package/src/lib/bpm-auth/bpm-auth.options.js.map +0 -1
- package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.d.ts +0 -10
- package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js +0 -40
- package/src/lib/bpm-auth/configurable-bpm-auth-context.accessor.js.map +0 -1
- package/src/lib/bpm-auth/index.js +0 -10
- package/src/lib/bpm-auth/index.js.map +0 -1
- package/src/lib/common/filters/all-exceptions.filter.d.ts +0 -25
- package/src/lib/common/filters/all-exceptions.filter.js +0 -93
- package/src/lib/common/filters/all-exceptions.filter.js.map +0 -1
- package/src/lib/common/index.js +0 -5
- package/src/lib/common/index.js.map +0 -1
- package/src/lib/common/iso-duration.d.ts +0 -1
- package/src/lib/common/iso-duration.js +0 -27
- package/src/lib/common/iso-duration.js.map +0 -1
- package/src/lib/condition/condition.module.d.ts +0 -2
- package/src/lib/condition/condition.module.js +0 -16
- package/src/lib/condition/condition.module.js.map +0 -1
- package/src/lib/condition/condition.service.d.ts +0 -16
- package/src/lib/condition/condition.service.js +0 -82
- package/src/lib/condition/condition.service.js.map +0 -1
- package/src/lib/condition/index.js +0 -6
- package/src/lib/condition/index.js.map +0 -1
- package/src/lib/database/data-source.d.ts +0 -4
- package/src/lib/database/data-source.js +0 -7
- package/src/lib/database/data-source.js.map +0 -1
- package/src/lib/database/index.js +0 -6
- package/src/lib/database/index.js.map +0 -1
- package/src/lib/database/migration-runner.d.ts +0 -1
- package/src/lib/database/migration-runner.js +0 -23
- package/src/lib/database/migration-runner.js.map +0 -1
- package/src/lib/database/typeorm.config.d.ts +0 -14
- package/src/lib/database/typeorm.config.js +0 -135
- package/src/lib/database/typeorm.config.js.map +0 -1
- package/src/lib/delegation/delegation-rule.entity.d.ts +0 -19
- package/src/lib/delegation/delegation-rule.entity.js +0 -99
- package/src/lib/delegation/delegation-rule.entity.js.map +0 -1
- package/src/lib/delegation/delegation.enums.d.ts +0 -10
- package/src/lib/delegation/delegation.enums.js +0 -23
- package/src/lib/delegation/delegation.enums.js.map +0 -1
- package/src/lib/delegation/delegation.module.d.ts +0 -2
- package/src/lib/delegation/delegation.module.js +0 -22
- package/src/lib/delegation/delegation.module.js.map +0 -1
- package/src/lib/delegation/delegation.mutations.d.ts +0 -11
- package/src/lib/delegation/delegation.mutations.js +0 -87
- package/src/lib/delegation/delegation.mutations.js.map +0 -1
- package/src/lib/delegation/delegation.queries.d.ts +0 -10
- package/src/lib/delegation/delegation.queries.js +0 -91
- package/src/lib/delegation/delegation.queries.js.map +0 -1
- package/src/lib/delegation/delegation.service.d.ts +0 -56
- package/src/lib/delegation/delegation.service.js +0 -268
- package/src/lib/delegation/delegation.service.js.map +0 -1
- package/src/lib/delegation/dto/delegation-rule.input.d.ts +0 -24
- package/src/lib/delegation/dto/delegation-rule.input.js +0 -143
- package/src/lib/delegation/dto/delegation-rule.input.js.map +0 -1
- package/src/lib/delegation/index.js +0 -9
- package/src/lib/delegation/index.js.map +0 -1
- package/src/lib/form/dto/form-definition.input.d.ts +0 -21
- package/src/lib/form/dto/form-definition.input.js +0 -103
- package/src/lib/form/dto/form-definition.input.js.map +0 -1
- package/src/lib/form/form-definition-version.entity.d.ts +0 -17
- package/src/lib/form/form-definition-version.entity.js +0 -84
- package/src/lib/form/form-definition-version.entity.js.map +0 -1
- package/src/lib/form/form-definition.entity.d.ts +0 -13
- package/src/lib/form/form-definition.entity.js +0 -66
- package/src/lib/form/form-definition.entity.js.map +0 -1
- package/src/lib/form/form-schema-lint.object.d.ts +0 -4
- package/src/lib/form/form-schema-lint.object.js +0 -20
- package/src/lib/form/form-schema-lint.object.js.map +0 -1
- package/src/lib/form/form-schema.validator.d.ts +0 -13
- package/src/lib/form/form-schema.validator.js +0 -354
- package/src/lib/form/form-schema.validator.js.map +0 -1
- package/src/lib/form/form.enums.d.ts +0 -9
- package/src/lib/form/form.enums.js +0 -22
- package/src/lib/form/form.enums.js.map +0 -1
- package/src/lib/form/form.module.d.ts +0 -2
- package/src/lib/form/form.module.js +0 -27
- package/src/lib/form/form.module.js.map +0 -1
- package/src/lib/form/form.mutations.d.ts +0 -14
- package/src/lib/form/form.mutations.js +0 -88
- package/src/lib/form/form.mutations.js.map +0 -1
- package/src/lib/form/form.queries.d.ts +0 -16
- package/src/lib/form/form.queries.js +0 -96
- package/src/lib/form/form.queries.js.map +0 -1
- package/src/lib/form/form.service.d.ts +0 -35
- package/src/lib/form/form.service.js +0 -297
- package/src/lib/form/form.service.js.map +0 -1
- package/src/lib/form/index.js +0 -12
- package/src/lib/form/index.js.map +0 -1
- package/src/lib/identity/identity-options.d.ts +0 -13
- package/src/lib/identity/identity-options.js +0 -20
- package/src/lib/identity/identity-options.js.map +0 -1
- package/src/lib/identity/identity.module.d.ts +0 -17
- package/src/lib/identity/identity.module.js +0 -65
- package/src/lib/identity/identity.module.js.map +0 -1
- package/src/lib/identity/identity.queries.d.ts +0 -12
- package/src/lib/identity/identity.queries.js +0 -79
- package/src/lib/identity/identity.queries.js.map +0 -1
- package/src/lib/identity/identity.service.d.ts +0 -22
- package/src/lib/identity/identity.service.js +0 -141
- package/src/lib/identity/identity.service.js.map +0 -1
- package/src/lib/identity/index.js +0 -7
- package/src/lib/identity/index.js.map +0 -1
- package/src/lib/identity/member-base.adapter.d.ts +0 -95
- package/src/lib/identity/member-base.adapter.js +0 -137
- package/src/lib/identity/member-base.adapter.js.map +0 -1
- package/src/lib/identity/member-metadata-cache.entity.d.ts +0 -8
- package/src/lib/identity/member-metadata-cache.entity.js +0 -39
- package/src/lib/identity/member-metadata-cache.entity.js.map +0 -1
- package/src/lib/identity/member-profile.object.d.ts +0 -8
- package/src/lib/identity/member-profile.object.js +0 -37
- package/src/lib/identity/member-profile.object.js.map +0 -1
- package/src/lib/identity/member-resolver.interface.js +0 -19
- package/src/lib/identity/member-resolver.interface.js.map +0 -1
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.d.ts +0 -6
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.js +0 -17
- package/src/lib/migrations/0000000000001-enable-postgres-extensions.js.map +0 -1
- package/src/lib/migrations/0000000001000-identity-organization-foundation.d.ts +0 -6
- package/src/lib/migrations/0000000001000-identity-organization-foundation.js.map +0 -1
- package/src/lib/migrations/0000000002000-form-builder-foundation.d.ts +0 -6
- package/src/lib/migrations/0000000002000-form-builder-foundation.js.map +0 -1
- package/src/lib/migrations/0000000003000-approval-template-foundation.d.ts +0 -6
- package/src/lib/migrations/0000000003000-approval-template-foundation.js.map +0 -1
- package/src/lib/migrations/0000000004000-workflow-engine-foundation.d.ts +0 -6
- package/src/lib/migrations/0000000004000-workflow-engine-foundation.js.map +0 -1
- package/src/lib/migrations/0000000005000-delegation-rules.d.ts +0 -6
- package/src/lib/migrations/0000000005000-delegation-rules.js.map +0 -1
- package/src/lib/migrations/0000000006000-notifications-sla.d.ts +0 -6
- package/src/lib/migrations/0000000006000-notifications-sla.js.map +0 -1
- package/src/lib/migrations/0000000007000-signatures-attachments.d.ts +0 -6
- package/src/lib/migrations/0000000007000-signatures-attachments.js.map +0 -1
- package/src/lib/migrations/0000000008000-approval-template-categories.d.ts +0 -6
- package/src/lib/migrations/0000000008000-approval-template-categories.js.map +0 -1
- package/src/lib/migrations/0000000009000-task-candidates.d.ts +0 -6
- package/src/lib/migrations/0000000009000-task-candidates.js.map +0 -1
- package/src/lib/migrations/0000000010000-notification-delivery-state.d.ts +0 -6
- package/src/lib/migrations/0000000010000-notification-delivery-state.js.map +0 -1
- package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.d.ts +0 -6
- package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js +0 -22
- package/src/lib/migrations/0000000011000-remove-attachment-encryption-key.js.map +0 -1
- package/src/lib/migrations/0000000012000-notification-sla-idempotency.d.ts +0 -6
- package/src/lib/migrations/0000000012000-notification-sla-idempotency.js +0 -21
- package/src/lib/migrations/0000000012000-notification-sla-idempotency.js.map +0 -1
- package/src/lib/migrations/0000000013000-workflow-query-indexes.d.ts +0 -6
- package/src/lib/migrations/0000000013000-workflow-query-indexes.js +0 -80
- package/src/lib/migrations/0000000013000-workflow-query-indexes.js.map +0 -1
- package/src/lib/migrations/index.d.ts +0 -16
- package/src/lib/migrations/index.js +0 -49
- package/src/lib/migrations/index.js.map +0 -1
- package/src/lib/notification/dto/notification-preference.input.d.ts +0 -9
- package/src/lib/notification/dto/notification-preference.input.js +0 -53
- package/src/lib/notification/dto/notification-preference.input.js.map +0 -1
- package/src/lib/notification/index.js +0 -11
- package/src/lib/notification/index.js.map +0 -1
- package/src/lib/notification/notification-delivery-scheduler.service.d.ts +0 -13
- package/src/lib/notification/notification-delivery-scheduler.service.js +0 -53
- package/src/lib/notification/notification-delivery-scheduler.service.js.map +0 -1
- package/src/lib/notification/notification-delivery.service.d.ts +0 -23
- package/src/lib/notification/notification-delivery.service.js +0 -245
- package/src/lib/notification/notification-delivery.service.js.map +0 -1
- package/src/lib/notification/notification-dispatcher.token.d.ts +0 -7
- package/src/lib/notification/notification-dispatcher.token.js +0 -5
- package/src/lib/notification/notification-dispatcher.token.js.map +0 -1
- package/src/lib/notification/notification-options.d.ts +0 -215
- package/src/lib/notification/notification-options.js +0 -128
- package/src/lib/notification/notification-options.js.map +0 -1
- package/src/lib/notification/notification-options.module.d.ts +0 -23
- package/src/lib/notification/notification-options.module.js +0 -41
- package/src/lib/notification/notification-options.module.js.map +0 -1
- package/src/lib/notification/notification-preference.entity.d.ts +0 -10
- package/src/lib/notification/notification-preference.entity.js +0 -50
- package/src/lib/notification/notification-preference.entity.js.map +0 -1
- package/src/lib/notification/notification-sla-scheduler.service.d.ts +0 -13
- package/src/lib/notification/notification-sla-scheduler.service.js +0 -51
- package/src/lib/notification/notification-sla-scheduler.service.js.map +0 -1
- package/src/lib/notification/notification-template.d.ts +0 -13
- package/src/lib/notification/notification-template.js +0 -61
- package/src/lib/notification/notification-template.js.map +0 -1
- package/src/lib/notification/notification.entity.d.ts +0 -23
- package/src/lib/notification/notification.entity.js +0 -122
- package/src/lib/notification/notification.entity.js.map +0 -1
- package/src/lib/notification/notification.enums.d.ts +0 -24
- package/src/lib/notification/notification.enums.js +0 -45
- package/src/lib/notification/notification.enums.js.map +0 -1
- package/src/lib/notification/notification.module.d.ts +0 -2
- package/src/lib/notification/notification.module.js +0 -45
- package/src/lib/notification/notification.module.js.map +0 -1
- package/src/lib/notification/notification.mutations.d.ts +0 -11
- package/src/lib/notification/notification.mutations.js +0 -64
- package/src/lib/notification/notification.mutations.js.map +0 -1
- package/src/lib/notification/notification.queries.d.ts +0 -11
- package/src/lib/notification/notification.queries.js +0 -77
- package/src/lib/notification/notification.queries.js.map +0 -1
- package/src/lib/notification/notification.service.d.ts +0 -81
- package/src/lib/notification/notification.service.js +0 -621
- package/src/lib/notification/notification.service.js.map +0 -1
- package/src/lib/organization/dto/manager-resolution.input.d.ts +0 -18
- package/src/lib/organization/dto/manager-resolution.input.js +0 -107
- package/src/lib/organization/dto/manager-resolution.input.js.map +0 -1
- package/src/lib/organization/dto/membership.input.d.ts +0 -16
- package/src/lib/organization/dto/membership.input.js +0 -99
- package/src/lib/organization/dto/membership.input.js.map +0 -1
- package/src/lib/organization/dto/org-unit.input.d.ts +0 -24
- package/src/lib/organization/dto/org-unit.input.js +0 -119
- package/src/lib/organization/dto/org-unit.input.js.map +0 -1
- package/src/lib/organization/dto/position.input.d.ts +0 -13
- package/src/lib/organization/dto/position.input.js +0 -70
- package/src/lib/organization/dto/position.input.js.map +0 -1
- package/src/lib/organization/index.js +0 -16
- package/src/lib/organization/index.js.map +0 -1
- package/src/lib/organization/json-metadata.d.ts +0 -1
- package/src/lib/organization/json-metadata.js +0 -14
- package/src/lib/organization/json-metadata.js.map +0 -1
- package/src/lib/organization/manager-resolution.entity.d.ts +0 -11
- package/src/lib/organization/manager-resolution.entity.js +0 -55
- package/src/lib/organization/manager-resolution.entity.js.map +0 -1
- package/src/lib/organization/membership.entity.d.ts +0 -11
- package/src/lib/organization/membership.entity.js +0 -60
- package/src/lib/organization/membership.entity.js.map +0 -1
- package/src/lib/organization/org-unit-tree-commit-result.object.d.ts +0 -4
- package/src/lib/organization/org-unit-tree-commit-result.object.js +0 -17
- package/src/lib/organization/org-unit-tree-commit-result.object.js.map +0 -1
- package/src/lib/organization/org-unit.entity.d.ts +0 -13
- package/src/lib/organization/org-unit.entity.js +0 -64
- package/src/lib/organization/org-unit.entity.js.map +0 -1
- package/src/lib/organization/organization-summary.object.d.ts +0 -6
- package/src/lib/organization/organization-summary.object.js +0 -28
- package/src/lib/organization/organization-summary.object.js.map +0 -1
- package/src/lib/organization/organization.enums.d.ts +0 -11
- package/src/lib/organization/organization.enums.js +0 -22
- package/src/lib/organization/organization.enums.js.map +0 -1
- package/src/lib/organization/organization.module.d.ts +0 -2
- package/src/lib/organization/organization.module.js +0 -35
- package/src/lib/organization/organization.module.js.map +0 -1
- package/src/lib/organization/organization.mutations.d.ts +0 -26
- package/src/lib/organization/organization.mutations.js +0 -148
- package/src/lib/organization/organization.mutations.js.map +0 -1
- package/src/lib/organization/organization.queries.d.ts +0 -23
- package/src/lib/organization/organization.queries.js +0 -202
- package/src/lib/organization/organization.queries.js.map +0 -1
- package/src/lib/organization/organization.service.d.ts +0 -102
- package/src/lib/organization/organization.service.js +0 -751
- package/src/lib/organization/organization.service.js.map +0 -1
- package/src/lib/organization/position.entity.d.ts +0 -9
- package/src/lib/organization/position.entity.js +0 -48
- package/src/lib/organization/position.entity.js.map +0 -1
- package/src/lib/organization/resolved-manager.object.d.ts +0 -4
- package/src/lib/organization/resolved-manager.object.js +0 -20
- package/src/lib/organization/resolved-manager.object.js.map +0 -1
- package/src/lib/signature/index.js +0 -8
- package/src/lib/signature/index.js.map +0 -1
- package/src/lib/signature/signature-options.d.ts +0 -33
- package/src/lib/signature/signature-options.js +0 -51
- package/src/lib/signature/signature-options.js.map +0 -1
- package/src/lib/signature/signature-verification.object.d.ts +0 -6
- package/src/lib/signature/signature-verification.object.js +0 -28
- package/src/lib/signature/signature-verification.object.js.map +0 -1
- package/src/lib/signature/signature.entity.d.ts +0 -16
- package/src/lib/signature/signature.entity.js +0 -88
- package/src/lib/signature/signature.entity.js.map +0 -1
- package/src/lib/signature/signature.module.d.ts +0 -11
- package/src/lib/signature/signature.module.js +0 -60
- package/src/lib/signature/signature.module.js.map +0 -1
- package/src/lib/signature/signature.queries.d.ts +0 -11
- package/src/lib/signature/signature.queries.js +0 -46
- package/src/lib/signature/signature.queries.js.map +0 -1
- package/src/lib/signature/signature.service.d.ts +0 -25
- package/src/lib/signature/signature.service.js +0 -124
- package/src/lib/signature/signature.service.js.map +0 -1
- package/src/lib/template/approval-template-category.entity.d.ts +0 -9
- package/src/lib/template/approval-template-category.entity.js +0 -49
- package/src/lib/template/approval-template-category.entity.js.map +0 -1
- package/src/lib/template/approval-template-version.entity.d.ts +0 -21
- package/src/lib/template/approval-template-version.entity.js +0 -106
- package/src/lib/template/approval-template-version.entity.js.map +0 -1
- package/src/lib/template/approval-template.entity.d.ts +0 -14
- package/src/lib/template/approval-template.entity.js +0 -74
- package/src/lib/template/approval-template.entity.js.map +0 -1
- package/src/lib/template/dto/approval-template.input.d.ts +0 -36
- package/src/lib/template/dto/approval-template.input.js +0 -187
- package/src/lib/template/dto/approval-template.input.js.map +0 -1
- package/src/lib/template/index.js +0 -12
- package/src/lib/template/index.js.map +0 -1
- package/src/lib/template/template.enums.d.ts +0 -14
- package/src/lib/template/template.enums.js +0 -31
- package/src/lib/template/template.enums.js.map +0 -1
- package/src/lib/template/template.module.d.ts +0 -2
- package/src/lib/template/template.module.js +0 -33
- package/src/lib/template/template.module.js.map +0 -1
- package/src/lib/template/template.mutations.d.ts +0 -20
- package/src/lib/template/template.mutations.js +0 -139
- package/src/lib/template/template.mutations.js.map +0 -1
- package/src/lib/template/template.queries.d.ts +0 -16
- package/src/lib/template/template.queries.js +0 -133
- package/src/lib/template/template.queries.js.map +0 -1
- package/src/lib/template/template.service.d.ts +0 -64
- package/src/lib/template/template.service.js +0 -521
- package/src/lib/template/template.service.js.map +0 -1
- package/src/lib/template/workflow-definition.validator.d.ts +0 -9
- package/src/lib/template/workflow-definition.validator.js +0 -362
- package/src/lib/template/workflow-definition.validator.js.map +0 -1
- package/src/lib/testing/cel-js.jest.d.ts +0 -15
- package/src/lib/testing/cel-js.jest.js +0 -27
- package/src/lib/testing/cel-js.jest.js.map +0 -1
- package/src/lib/workflow-engine/activity-log.entity.d.ts +0 -12
- package/src/lib/workflow-engine/activity-log.entity.js +0 -62
- package/src/lib/workflow-engine/activity-log.entity.js.map +0 -1
- package/src/lib/workflow-engine/approval-instance-page-info.object.d.ts +0 -8
- package/src/lib/workflow-engine/approval-instance-page-info.object.js +0 -36
- package/src/lib/workflow-engine/approval-instance-page-info.object.js.map +0 -1
- package/src/lib/workflow-engine/approval-instance.entity.d.ts +0 -22
- package/src/lib/workflow-engine/approval-instance.entity.js +0 -113
- package/src/lib/workflow-engine/approval-instance.entity.js.map +0 -1
- package/src/lib/workflow-engine/dto/cancel-approval-instance.input.d.ts +0 -5
- package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js +0 -29
- package/src/lib/workflow-engine/dto/cancel-approval-instance.input.js.map +0 -1
- package/src/lib/workflow-engine/dto/decide-task.input.d.ts +0 -9
- package/src/lib/workflow-engine/dto/decide-task.input.js +0 -47
- package/src/lib/workflow-engine/dto/decide-task.input.js.map +0 -1
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.d.ts +0 -6
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js +0 -34
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js.map +0 -1
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.d.ts +0 -6
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js +0 -34
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js.map +0 -1
- package/src/lib/workflow-engine/dto/submit-approval-instance.input.d.ts +0 -7
- package/src/lib/workflow-engine/dto/submit-approval-instance.input.js +0 -40
- package/src/lib/workflow-engine/dto/submit-approval-instance.input.js.map +0 -1
- package/src/lib/workflow-engine/index.js +0 -24
- package/src/lib/workflow-engine/index.js.map +0 -1
- package/src/lib/workflow-engine/task-candidate.entity.d.ts +0 -14
- package/src/lib/workflow-engine/task-candidate.entity.js +0 -72
- package/src/lib/workflow-engine/task-candidate.entity.js.map +0 -1
- package/src/lib/workflow-engine/task-decision.entity.d.ts +0 -12
- package/src/lib/workflow-engine/task-decision.entity.js +0 -60
- package/src/lib/workflow-engine/task-decision.entity.js.map +0 -1
- package/src/lib/workflow-engine/task.entity.d.ts +0 -20
- package/src/lib/workflow-engine/task.entity.js +0 -109
- package/src/lib/workflow-engine/task.entity.js.map +0 -1
- package/src/lib/workflow-engine/workflow-condition-evaluator.d.ts +0 -5
- package/src/lib/workflow-engine/workflow-condition-evaluator.js +0 -107
- package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +0 -1
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.d.ts +0 -8
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js +0 -36
- package/src/lib/workflow-engine/workflow-dashboard-summary.object.js.map +0 -1
- package/src/lib/workflow-engine/workflow-dry-run.object.d.ts +0 -21
- package/src/lib/workflow-engine/workflow-dry-run.object.js +0 -86
- package/src/lib/workflow-engine/workflow-dry-run.object.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.enums.d.ts +0 -58
- package/src/lib/workflow-engine/workflow-engine.enums.js +0 -95
- package/src/lib/workflow-engine/workflow-engine.enums.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.module.d.ts +0 -9
- package/src/lib/workflow-engine/workflow-engine.module.js +0 -76
- package/src/lib/workflow-engine/workflow-engine.module.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +0 -19
- package/src/lib/workflow-engine/workflow-engine.mutations.js +0 -106
- package/src/lib/workflow-engine/workflow-engine.mutations.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.queries.d.ts +0 -29
- package/src/lib/workflow-engine/workflow-engine.queries.js +0 -225
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +0 -145
- package/src/lib/workflow-engine/workflow-engine.service.js +0 -2785
- package/src/lib/workflow-engine/workflow-engine.service.js.map +0 -1
- package/src/lib/workflow-engine/workflow-engine.tokens.js +0 -5
- package/src/lib/workflow-engine/workflow-engine.tokens.js.map +0 -1
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.d.ts +0 -18
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +0 -39
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +0 -1
- package/src/lib/workflow-engine/workflow-token.entity.d.ts +0 -10
- package/src/lib/workflow-engine/workflow-token.entity.js +0 -50
- package/src/lib/workflow-engine/workflow-token.entity.js.map +0 -1
- /package/src/{index.d.ts → index.ts} +0 -0
- /package/src/lib/attachment/{index.d.ts → index.ts} +0 -0
- /package/src/lib/bpm/{index.d.ts → index.ts} +0 -0
- /package/src/lib/bpm-auth/{index.d.ts → index.ts} +0 -0
- /package/src/lib/common/{index.d.ts → index.ts} +0 -0
- /package/src/lib/condition/{index.d.ts → index.ts} +0 -0
- /package/src/lib/database/{index.d.ts → index.ts} +0 -0
- /package/src/lib/delegation/{index.d.ts → index.ts} +0 -0
- /package/src/lib/form/{index.d.ts → index.ts} +0 -0
- /package/src/lib/identity/{index.d.ts → index.ts} +0 -0
- /package/src/lib/notification/{index.d.ts → index.ts} +0 -0
- /package/src/lib/organization/{index.d.ts → index.ts} +0 -0
- /package/src/lib/signature/{index.d.ts → index.ts} +0 -0
- /package/src/lib/template/{index.d.ts → index.ts} +0 -0
- /package/src/lib/workflow-engine/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,4707 @@
|
|
|
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
|
+
}
|