@rytass/bpm-core-nestjs-module 0.17.9 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +29 -2
  3. package/package.json +2 -2
  4. package/src/lib/delegation/delegation.service.d.ts +41 -1
  5. package/src/lib/delegation/delegation.service.js +147 -23
  6. package/src/lib/delegation/delegation.service.js.map +1 -1
  7. package/src/lib/form/form-org-unit-value.validator.d.ts +21 -0
  8. package/src/lib/form/form-org-unit-value.validator.js +43 -0
  9. package/src/lib/form/form-org-unit-value.validator.js.map +1 -0
  10. package/src/lib/form/form-schema.validator.js +83 -6
  11. package/src/lib/form/form-schema.validator.js.map +1 -1
  12. package/src/lib/notification/notification.service.js +3 -1
  13. package/src/lib/notification/notification.service.js.map +1 -1
  14. package/src/lib/organization/dto/org-unit-option.object.d.ts +13 -0
  15. package/src/lib/organization/dto/org-unit-option.object.js +35 -0
  16. package/src/lib/organization/dto/org-unit-option.object.js.map +1 -0
  17. package/src/lib/organization/index.d.ts +2 -0
  18. package/src/lib/organization/index.js +2 -0
  19. package/src/lib/organization/index.js.map +1 -1
  20. package/src/lib/organization/organization-picker.queries.d.ts +17 -0
  21. package/src/lib/organization/organization-picker.queries.js +50 -0
  22. package/src/lib/organization/organization-picker.queries.js.map +1 -0
  23. package/src/lib/organization/organization.module.js +2 -0
  24. package/src/lib/organization/organization.module.js.map +1 -1
  25. package/src/lib/organization/organization.service.d.ts +17 -0
  26. package/src/lib/organization/organization.service.js +61 -0
  27. package/src/lib/organization/organization.service.js.map +1 -1
  28. package/src/lib/template/template.service.js +73 -4
  29. package/src/lib/template/template.service.js.map +1 -1
  30. package/src/lib/template/workflow-definition.validator.js +58 -8
  31. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  32. package/src/lib/workflow-engine/text-field-pattern-test.d.ts +23 -0
  33. package/src/lib/workflow-engine/text-field-pattern-test.js +75 -0
  34. package/src/lib/workflow-engine/text-field-pattern-test.js.map +1 -0
  35. package/src/lib/workflow-engine/workflow-engine.service.d.ts +59 -0
  36. package/src/lib/workflow-engine/workflow-engine.service.js +386 -41
  37. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
@@ -18,6 +18,7 @@ const form_definition_version_entity_1 = require("../form/form-definition-versio
18
18
  const form_enums_1 = require("../form/form.enums");
19
19
  const form_data_source_errors_1 = require("../form-data-source/form-data-source.errors");
20
20
  const form_member_value_validator_1 = require("../form/form-member-value.validator");
21
+ const form_org_unit_value_validator_1 = require("../form/form-org-unit-value.validator");
21
22
  const form_data_source_types_1 = require("../form-data-source/form-data-source.types");
22
23
  const sla_schedule_service_1 = require("../calendar/sla-schedule.service");
23
24
  const notification_service_1 = require("../notification/notification.service");
@@ -38,6 +39,7 @@ const activity_log_entity_1 = require("./activity-log.entity");
38
39
  const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
39
40
  const adhoc_enums_1 = require("./adhoc.enums");
40
41
  const approval_instance_entity_1 = require("./approval-instance.entity");
42
+ const text_field_pattern_test_1 = require("./text-field-pattern-test");
41
43
  const form_data_fingerprint_1 = require("./form-data-fingerprint");
42
44
  const instance_submitter_1 = require("./instance-submitter");
43
45
  const task_decision_entity_1 = require("./task-decision.entity");
@@ -681,6 +683,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
681
683
  decisionId: decision.id,
682
684
  signatureId: signature.id,
683
685
  signedPayloadHash: signature.signedPayloadHash,
686
+ // Absent for a person's decision, so "who actually signed" can
687
+ // tell the engine's own timeout approvals apart.
688
+ ...(options.automatic ? { automatic: options.automatic } : {}),
684
689
  // Keys only: this payload is forwarded to hosts, and the values
685
690
  // can be tax IDs or amounts.
686
691
  ...(changedFieldKeys.length > 0 ? { changedFieldKeys } : {}),
@@ -1825,6 +1830,16 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1825
1830
  resolveDryRunAssignment(input, node) {
1826
1831
  const resolver = node.data.approverResolver;
1827
1832
  const excludeInitiator = node.data.excludeInitiator;
1833
+ const unsupportedType = (0, workflow_1.readUnsupportedApproverResolverType)(resolver);
1834
+ // A source from a newer engine cannot be simulated. Say so and end the
1835
+ // branch instead of showing its type name as if it were a person.
1836
+ if (unsupportedType !== null) {
1837
+ return {
1838
+ assigneeMemberId: null,
1839
+ message: `簽核節點「${readDryRunNodeName(node)}」此引擎版本不支援簽核人來源「${unsupportedType}」,請升級 BPMCore 後再試`,
1840
+ outcome: 'STOPPED',
1841
+ };
1842
+ }
1828
1843
  const memberIds = this.readDryRunMemberIds(input, resolver, `workflow.nodes.${node.id}.data.approverResolver`);
1829
1844
  if (memberIds === null) {
1830
1845
  return {
@@ -1910,6 +1925,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1910
1925
  ? managerMemberIds
1911
1926
  : readDryRunFallbackMemberIds(input, resolver.fallback);
1912
1927
  }
1928
+ // The form pick, the unit's rules and an earlier node's outcome are not
1929
+ // known before the instance runs: these stand in as placeholders.
1913
1930
  return null;
1914
1931
  }
1915
1932
  async getApprovalInstance(id, scope) {
@@ -3840,11 +3857,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3840
3857
  // Not deduplicated: a node that excludes the initiator filters first, so a
3841
3858
  // first-seen entry that must be dropped cannot shadow a legitimate one.
3842
3859
  async delegateResolvedCandidates(manager, instance, nodeId, resolvedCandidates, gradeGated) {
3843
- const context = this.buildDelegationContext(instance, nodeId);
3844
- const resolved = await Promise.all(resolvedCandidates.map(async (candidate) => ({
3860
+ // One batch for the node, so the host directory is asked once for every
3861
+ // candidate's delegation agents rather than once per candidate.
3862
+ const resolutions = await this.delegationService.resolveAssignees(resolvedCandidates.map((candidate) => candidate.memberId), this.buildDelegationContext(instance, nodeId));
3863
+ const resolved = resolvedCandidates.map((candidate, index) => ({
3845
3864
  candidate,
3846
- resolution: await this.delegationService.resolveAssignee(candidate.memberId, context),
3847
- })));
3865
+ resolution: resolutions[index],
3866
+ }));
3848
3867
  // One batch of grade lookups for the whole node, not two per delegate.
3849
3868
  const levels = gradeGated
3850
3869
  ? await this.readMemberPositionLevels(manager, resolved.flatMap(({ candidate, resolution }) => readGradeGateMemberIds(candidate.memberId, resolution)), this.readOrganizationToday())
@@ -3903,31 +3922,175 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3903
3922
  : null;
3904
3923
  }
3905
3924
  async resolveApproverResolver(manager, instance, resolver, label, subject = '簽核人', skippedChainMemberIds = []) {
3925
+ const memberIds = await this.resolveApproverMemberIds(manager, instance, resolver, label, subject, skippedChainMemberIds);
3926
+ return memberIds.map((memberId) => ({
3927
+ memberId,
3928
+ sourceType: resolver.type,
3929
+ }));
3930
+ }
3931
+ /** The member ids `resolver` routes to; every source type, one branch each. */
3932
+ async resolveApproverMemberIds(manager, instance, resolver, label, subject, skippedChainMemberIds) {
3906
3933
  if (resolver.type === 'DIRECT') {
3907
- return readMemberIdsFromValue(resolver.memberIds, label, subject).map((memberId) => ({ memberId, sourceType: resolver.type }));
3934
+ return readMemberIdsFromValue(resolver.memberIds, label, subject);
3908
3935
  }
3909
3936
  if (resolver.type === 'DYNAMIC_FORM') {
3910
- return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`).map((memberId) => ({ memberId, sourceType: resolver.type }));
3937
+ return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`);
3911
3938
  }
3912
3939
  if (resolver.type === 'EXPRESSION') {
3913
- return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`), label, subject, '依運算式').map((memberId) => ({ memberId, sourceType: resolver.type }));
3940
+ return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, buildWorkflowExpressionContext(instance), `${label}.expression`), label, subject, '依運算式');
3914
3941
  }
3915
3942
  if (resolver.type === 'POSITION') {
3916
- return this.resolvePositionCandidates(manager, resolver.positionId, label, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
3943
+ return this.resolvePositionCandidates(manager, resolver.positionId, label, subject);
3917
3944
  }
3918
3945
  if (resolver.type === 'ORG_UNIT_MEMBER') {
3919
- return this.resolveOrgUnitMemberCandidates(manager, resolver.orgUnitId, Boolean(resolver.includeDescendants), label, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
3946
+ return this.resolveOrgUnitMemberCandidates(manager, resolver.orgUnitId, Boolean(resolver.includeDescendants), label, subject);
3920
3947
  }
3921
3948
  if (resolver.type === 'ORG_UNIT_POSITION') {
3922
- return this.resolveOrgUnitPositionCandidates(manager, resolver.orgUnitId, resolver.positionId, Boolean(resolver.includeDescendants), label, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
3949
+ return this.resolveOrgUnitPositionCandidates(manager, resolver.orgUnitId, resolver.positionId, Boolean(resolver.includeDescendants), label, subject);
3923
3950
  }
3924
3951
  if (resolver.type === 'ORG_UNIT_MANAGER') {
3925
- return this.resolveOrgUnitManagerCandidates(manager, instance, resolver.orgUnitId, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
3952
+ return this.resolveOrgUnitManagerCandidates(manager, instance, resolver.orgUnitId, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject);
3926
3953
  }
3927
3954
  if (resolver.type === 'ORG_MANAGER_CHAIN') {
3928
- return this.resolveOrgManagerChainCandidates(manager, instance, resolver.minPositionLevel, resolver.fallback, label, subject, skippedChainMemberIds).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
3955
+ return this.resolveOrgManagerChainCandidates(manager, instance, resolver.minPositionLevel, resolver.fallback, label, subject, skippedChainMemberIds);
3956
+ }
3957
+ if (resolver.type === 'ORG_MANAGER') {
3958
+ return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject);
3959
+ }
3960
+ if (resolver.type === 'FORM_MEMBER_MANAGER') {
3961
+ return this.resolveFormMemberManagerCandidates(manager, instance, resolver, label, subject);
3962
+ }
3963
+ if (resolver.type === 'FORM_ORG_UNIT_MANAGER') {
3964
+ return this.resolveFormOrgUnitManagerCandidates(manager, instance, resolver, label, subject);
3965
+ }
3966
+ if (resolver.type === 'NODE_ACTOR') {
3967
+ return this.resolveNodeActorCandidates(manager, instance, resolver, label, subject);
3968
+ }
3969
+ if (resolver.type === 'NODE_ACTOR_MANAGER') {
3970
+ return this.resolveNodeActorManagerCandidates(manager, instance, resolver, label, subject);
3971
+ }
3972
+ // A template saved by a newer engine can carry an approver source this
3973
+ // engine does not know. Refuse it by name instead of quietly resolving it
3974
+ // as the initiator's manager, which would route the task to the wrong
3975
+ // person. That holds as well for a source a newer shared package knows
3976
+ // and this engine does not route yet, so the type is named either way.
3977
+ const unresolvedType = resolver.type;
3978
+ throw new common_1.ConflictException(`${label} 無法建立${subject === '通知對象' ? '通知' : '待簽任務'}:此引擎版本不支援簽核人來源「${unresolvedType}」。請升級 BPMCore 後再發佈或執行此流程。`);
3979
+ }
3980
+ /**
3981
+ * FORM_MEMBER_MANAGER: the manager of the one person picked on the form.
3982
+ * The picked person is the starting point and is kept out of every level's
3983
+ * result, so a unit head who is picked is handed to the tier above instead
3984
+ * of coming back as his own manager.
3985
+ */
3986
+ async resolveFormMemberManagerCandidates(manager, instance, resolver, label, subject) {
3987
+ const source = `依表單欄位「${resolver.formPath}」`;
3988
+ const pickedMemberIds = readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, source);
3989
+ // Picking one silently would route the document by accident.
3990
+ if (pickedMemberIds.length > 1) {
3991
+ throw new common_1.ConflictException(`${label} ${source}只能選一位人員`);
3992
+ }
3993
+ const levelsUp = readManagerLevelsUp(resolver.levelsUp);
3994
+ const managerMemberIds = await this.resolveManagerMemberIdsExcludingSubject(manager, pickedMemberIds[0], levelsUp, resolver.preferClosestOrgUnit === true);
3995
+ if (managerMemberIds.length > 0) {
3996
+ return managerMemberIds;
3997
+ }
3998
+ return this.resolveFallbackAssignees(manager, instance, resolver.fallback, label, subject, `找不到表單欄位「${resolver.formPath}」所選人員的第 ${levelsUp} 層主管`);
3999
+ }
4000
+ /**
4001
+ * FORM_ORG_UNIT_MANAGER: the manager of the organisation unit picked on the
4002
+ * form, through the same rules as a fixed ORG_UNIT_MANAGER. A deleted unit
4003
+ * resolves to nobody and takes the "no manager" path.
4004
+ */
4005
+ async resolveFormOrgUnitManagerCandidates(manager, instance, resolver, label, subject) {
4006
+ const orgUnitValue = readValueAtPath(instance.formData, resolver.formPath);
4007
+ // A value that is not a uuid would reach the uuid column of the org unit
4008
+ // lookup and surface as a database error instead of this refusal.
4009
+ if (typeof orgUnitValue !== 'string' ||
4010
+ !(0, form_org_unit_value_validator_1.isSubmittableOrgUnitValue)(orgUnitValue)) {
4011
+ throw new common_1.ConflictException(`${label} 依表單欄位「${resolver.formPath}」找不到所選組織`);
3929
4012
  }
3930
- return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
4013
+ return this.resolveOrgUnitManagerCandidates(manager, instance, orgUnitValue.trim(), resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject);
4014
+ }
4015
+ /**
4016
+ * NODE_ACTOR: the people who actually approved the listed earlier nodes.
4017
+ * A node nobody approved (never ran, cancelled, or approved only by the SLA
4018
+ * timeout) is an error rather than an empty list: handing the task to
4019
+ * nobody would stall the instance without saying why.
4020
+ */
4021
+ async resolveNodeActorCandidates(manager, instance, resolver, label, subject) {
4022
+ const actorMemberIds = await this.readNodeActorMemberIds(manager, instance, resolver.nodeIds);
4023
+ if (actorMemberIds.length === 0) {
4024
+ throw new common_1.ConflictException(`${label} 找不到${subject}:${readNodeActorNodeNames(instance, resolver.nodeIds)}沒有實際同意的簽核紀錄`);
4025
+ }
4026
+ return actorMemberIds;
4027
+ }
4028
+ /**
4029
+ * NODE_ACTOR_MANAGER: the managers `levelsUp` tiers above each person who
4030
+ * actually approved the listed nodes, as one set. An approver without a
4031
+ * manager is skipped; the fallback runs only when nobody is left.
4032
+ */
4033
+ async resolveNodeActorManagerCandidates(manager, instance, resolver, label, subject) {
4034
+ const levelsUp = readManagerLevelsUp(resolver.levelsUp);
4035
+ const actorMemberIds = await this.readNodeActorMemberIds(manager, instance, resolver.nodeIds);
4036
+ const managerGroups = await Promise.all(actorMemberIds.map((actorMemberId) => this.resolveManagerMemberIdsExcludingSubject(manager, actorMemberId, levelsUp, resolver.preferClosestOrgUnit === true)));
4037
+ const managerMemberIds = uniqueTexts(managerGroups.flat());
4038
+ if (managerMemberIds.length > 0) {
4039
+ return managerMemberIds;
4040
+ }
4041
+ return this.resolveFallbackAssignees(manager, instance, resolver.fallback, label, subject, actorMemberIds.length === 0
4042
+ ? `${readNodeActorNodeNames(instance, resolver.nodeIds)}沒有實際同意的簽核紀錄`
4043
+ : `找不到前面關卡實際簽核人的第 ${levelsUp} 層主管`);
4044
+ }
4045
+ /**
4046
+ * The people who approved the most recent batch of tasks among `nodeIds`,
4047
+ * earliest decision first, each once. Read through `manager` so an approval
4048
+ * written earlier in the same transaction (the one that is advancing the
4049
+ * instance right now) is visible.
4050
+ *
4051
+ * A batch is the non-ad-hoc tasks of one node on one token: a token never
4052
+ * passes a node twice (a return opens a new token, a transfer's replacement
4053
+ * task keeps the token), so a later round is a later batch. Tasks created in
4054
+ * the same transaction share a `created_at`, so every (node, token) tied for
4055
+ * newest counts as one batch. An older batch is never used in place of a
4056
+ * newest one nobody approved. Ad-hoc approvals, approvals on cancelled
4057
+ * tasks and approvals the SLA timeout made do not count.
4058
+ */
4059
+ async readNodeActorMemberIds(manager, instance, nodeIds) {
4060
+ const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
4061
+ where: { instanceId: instance.id, isAdhoc: false, nodeId: (0, typeorm_2.In)([...nodeIds]) },
4062
+ });
4063
+ if (tasks.length === 0) {
4064
+ return [];
4065
+ }
4066
+ const newestCreatedAt = Math.max(...tasks.map((task) => task.createdAt.getTime()));
4067
+ const newestTasks = tasks.filter((task) => task.createdAt.getTime() === newestCreatedAt);
4068
+ const batchTaskIds = tasks
4069
+ .filter((task) => task.status !== workflow_engine_enums_1.TaskStatusEnum.CANCELLED &&
4070
+ newestTasks.some((newest) => newest.nodeId === task.nodeId && newest.tokenId === task.tokenId))
4071
+ .map((task) => task.id);
4072
+ if (batchTaskIds.length === 0) {
4073
+ return [];
4074
+ }
4075
+ const [decisions, decidedLogs] = await Promise.all([
4076
+ manager.getRepository(task_decision_entity_1.TaskDecisionEntity).find({
4077
+ where: { action: workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED, taskId: (0, typeorm_2.In)(batchTaskIds) },
4078
+ }),
4079
+ manager.getRepository(activity_log_entity_1.ActivityLogEntity).find({
4080
+ where: {
4081
+ eventType: workflow_engine_enums_1.ActivityLogEventTypeEnum.TASK_DECIDED,
4082
+ instanceId: instance.id,
4083
+ taskId: (0, typeorm_2.In)(batchTaskIds),
4084
+ },
4085
+ }),
4086
+ ]);
4087
+ const automaticDecisionIds = new Set(decidedLogs
4088
+ .filter((log) => log.payload['automatic'] === SLA_AUTO_APPROVE_MARK)
4089
+ .map((log) => log.payload['decisionId']));
4090
+ return uniqueTexts([...decisions]
4091
+ .filter((decision) => !automaticDecisionIds.has(decision.id))
4092
+ .sort((first, second) => first.decidedAt.getTime() - second.decidedAt.getTime())
4093
+ .map((decision) => decision.decidedByMemberId));
3931
4094
  }
3932
4095
  async resolvePositionCandidates(manager, positionId, label, subject) {
3933
4096
  const memberIds = await this.readActivePositionMemberIds(manager, positionId);
@@ -4112,6 +4275,29 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4112
4275
  const nextLevelGroups = await Promise.all(directManagerMemberIds.map((managerMemberId) => this.resolveManagerMemberIdsAtLevel(manager, managerMemberId, date, remainingLevels - 1, preferClosestOrgUnit)));
4113
4276
  return uniqueTexts(nextLevelGroups.flat());
4114
4277
  }
4278
+ /**
4279
+ * The managers `levelsUp` tiers above `startMemberId`. Every tier is looked
4280
+ * up with the person it starts from excluded (as the manager chain does), so
4281
+ * a unit head falls through to the tier above, and anyone already walked
4282
+ * past — the starting person included — counts as nobody, so a pair of
4283
+ * mutual managers cannot loop back. Nobody at any tier returns `[]`; the
4284
+ * caller applies its own fallback.
4285
+ */
4286
+ async resolveManagerMemberIdsExcludingSubject(manager, startMemberId, levelsUp, preferClosestOrgUnit) {
4287
+ const date = this.readOrganizationToday();
4288
+ let walkedMemberIds = [startMemberId];
4289
+ let currentMemberIds = [startMemberId];
4290
+ for (let level = 0; level < levelsUp; level += 1) {
4291
+ const managerGroups = await Promise.all(currentMemberIds.map((memberId) => this.resolveDirectManagerMemberIds(manager, memberId, date, preferClosestOrgUnit, [memberId])));
4292
+ const nextMemberIds = uniqueTexts(managerGroups.flat()).filter((memberId) => !walkedMemberIds.includes(memberId));
4293
+ if (nextMemberIds.length === 0) {
4294
+ return [];
4295
+ }
4296
+ walkedMemberIds = [...walkedMemberIds, ...nextMemberIds];
4297
+ currentMemberIds = nextMemberIds;
4298
+ }
4299
+ return currentMemberIds;
4300
+ }
4115
4301
  async resolveDirectManagerMemberIds(manager, memberId, date, preferClosestOrgUnit, excludedManagerMemberIds = []) {
4116
4302
  const memberships = await manager.getRepository(membership_entity_1.MembershipEntity).find({
4117
4303
  where: { memberId },
@@ -4301,6 +4487,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4301
4487
  }
4302
4488
  if (action.type === 'SET_FORM_FIELD') {
4303
4489
  const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`);
4490
+ assertServiceTaskTextValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
4304
4491
  const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
4305
4492
  // A service task runs inside this transaction, so it cannot call a host
4306
4493
  // provider to re-resolve the label. Rather than leave the previous
@@ -4334,6 +4521,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4334
4521
  await this.advanceTokenToOutgoingNodes(manager, updatedInstance, token, node);
4335
4522
  return;
4336
4523
  }
4524
+ // The sources that read the form or an earlier node's outcome are not
4525
+ // offered to a notify node (the publish lint refuses them); a definition
4526
+ // that reaches here with one anyway is refused by name. A type this
4527
+ // engine does not know is left to the resolver's own refusal below.
4528
+ if ((0, workflow_1.readUnsupportedApproverResolverType)(action.recipients) === null &&
4529
+ !(0, workflow_1.isNotifyRecipientResolverType)(action.recipients.type)) {
4530
+ throw new common_1.ConflictException(`知會節點「${node.data.label}」不能使用簽核人來源「${action.recipients.type}」當收件人,請改用指定人員、職位、組織或主管`);
4531
+ }
4337
4532
  // A webhook-only NOTIFY node names nobody. Resolving its empty DIRECT
4338
4533
  // resolver would throw and roll back the submit or decision that reached
4339
4534
  // it, so skip member resolution; webhooks are delivered separately.
@@ -4752,6 +4947,34 @@ function readDataSourceFieldKeyAtPath(snapshot, fieldPath) {
4752
4947
  const field = schema.fields.find((candidate) => isRecord(candidate) && candidate.fieldKey === fieldKey);
4753
4948
  return field && (0, form_1.isFormDataSourceFieldDefinition)(field) ? field.fieldKey : null;
4754
4949
  }
4950
+ /**
4951
+ * Holds a `SET_FORM_FIELD` write to the length and format rules the form
4952
+ * applies to a filler, so a service task cannot store text the field would
4953
+ * have refused. Breaking them throws, which rolls back the submit or decision
4954
+ * that reached the node: the value is never half-written, and the node's value
4955
+ * has to be corrected. Only a top-level field can carry those rules — the
4956
+ * writer ignores an indexed path, and a nested record path names no field —
4957
+ * and an unreadable snapshot is left alone, as in
4958
+ * `readDataSourceFieldKeyAtPath`.
4959
+ */
4960
+ function assertServiceTaskTextValue(snapshot, fieldPath, value, nodeLabel) {
4961
+ const schema = snapshot?.schema;
4962
+ const segments = normalizeFormFieldPath(fieldPath);
4963
+ if (!isFormDefinitionSchema(schema) || segments.length !== 1) {
4964
+ return;
4965
+ }
4966
+ const field = schema.fields.find((candidate) => candidate.fieldKey === segments[0]);
4967
+ const errors = field
4968
+ ? readSubmittedTextValueErrors(field, value, {
4969
+ testPattern: (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)(),
4970
+ })
4971
+ : [];
4972
+ if (!field || errors.length === 0) {
4973
+ return;
4974
+ }
4975
+ throw new common_1.BadRequestException(`系統節點「${nodeLabel}」要寫入欄位「${field.label || field.fieldKey}」的值不符合該欄位的規則` +
4976
+ `(${errors.join('; ')}),這次操作沒有生效;請聯絡流程管理者修正此節點的寫入值`);
4977
+ }
4755
4978
  function readSnapshotWithoutField(snapshots, fieldKey) {
4756
4979
  return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => key !== fieldKey));
4757
4980
  }
@@ -4951,6 +5174,17 @@ function readWorkflowNodeLabel(workflow, nodeId) {
4951
5174
  ?.data.label;
4952
5175
  return typeof label === 'string' && label.trim() ? label : null;
4953
5176
  }
5177
+ /** A stored `levelsUp` as a whole number of tiers, at least one. */
5178
+ function readManagerLevelsUp(levelsUp) {
5179
+ return Number.isFinite(levelsUp) ? Math.max(Math.trunc(levelsUp), 1) : 1;
5180
+ }
5181
+ const SLA_AUTO_APPROVE_MARK = 'SLA_AUTO_APPROVE';
5182
+ /** 關卡「名稱」(、關卡「名稱」…) for an error line; a missing node shows its id. */
5183
+ function readNodeActorNodeNames(instance, nodeIds) {
5184
+ return nodeIds
5185
+ .map((nodeId) => `關卡「${readWorkflowNodeLabel(instance.workflowSnapshot, nodeId) ?? nodeId}」`)
5186
+ .join('、');
5187
+ }
4954
5188
  function readWorkflowNodeOrThrow(workflow, nodeId) {
4955
5189
  const node = workflow.nodes.find((candidate) => candidate.id === nodeId);
4956
5190
  if (!node) {
@@ -4998,6 +5232,12 @@ function readReturnTargetNodeId(workflow, currentNodeId, requestedNodeId) {
4998
5232
  throw new common_1.BadRequestException('returnToNodeId is required');
4999
5233
  }
5000
5234
  readWorkflowNodeOrThrow(workflow, trimmedRequestedNodeId);
5235
+ // Only the applicant or a stage the case can already have passed: every
5236
+ // other node would skip ahead, re-run a gateway, or — the end event —
5237
+ // approve the case outright.
5238
+ if (!(0, workflow_graph_1.readSelectableReturnTargetNodeIds)(workflow, currentNodeId).includes(trimmedRequestedNodeId)) {
5239
+ throw new common_1.ConflictException(`User task ${currentNodeId} can only return to the applicant or an earlier user task, not ${trimmedRequestedNodeId}`);
5240
+ }
5001
5241
  return trimmedRequestedNodeId;
5002
5242
  }
5003
5243
  if (trimmedRequestedNodeId &&
@@ -5645,11 +5885,13 @@ function readCandidateStatusForClosedTask(taskStatus) {
5645
5885
  /**
5646
5886
  * `deferredFieldKeys` are fields an approver fills in later: they are checked
5647
5887
  * for shape (a table is an array of well-formed rows, a member value is a
5648
- * member id) but never for being present, so an empty one cannot block a
5649
- * submission that has nobody to fill it yet.
5888
+ * member id, an org unit value is an organization unit id) but never for
5889
+ * being present, so an empty one cannot block a submission that has nobody to
5890
+ * fill it yet.
5650
5891
  */
5651
5892
  function validateSubmittedFormData(schema, formData, options = {}) {
5652
5893
  const deferredFieldKeys = options.deferredFieldKeys ?? new Set();
5894
+ const testPattern = (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)();
5653
5895
  const visibleFields = schema.fields.filter((field) => isSubmittedFormFieldVisible(field, schema.fields, formData));
5654
5896
  // Tables answer "is this filled?" by row count and report per-row problems
5655
5897
  // with an instance path, so they get their own pass instead of the flat
@@ -5660,35 +5902,60 @@ function validateSubmittedFormData(schema, formData, options = {}) {
5660
5902
  !isSubmittedFieldValuePresent(formData[field.fieldKey]));
5661
5903
  throwFirstFormDataFieldError({
5662
5904
  missingFields,
5905
+ // A field an approver fills in later is held to its rules when they fill
5906
+ // it, not here: the applicant cannot see or change it, so only its type is
5907
+ // checked, the same as a table nobody fills in.
5908
+ readTextErrors: () => [
5909
+ ...visibleFields.flatMap((field) => readSubmittedTextValueErrors(field, formData[field.fieldKey], {
5910
+ shapeOnly: deferredFieldKeys.has(field.fieldKey),
5911
+ testPattern,
5912
+ })),
5913
+ // A field nobody can fill in right now — hidden (store-only), or one
5914
+ // whose `visibleWhen` is false — keeps its submitted value, so its value
5915
+ // is checked for its type. Only the type: its length and format apply
5916
+ // once the field is shown and the filler can correct it.
5917
+ ...schema.fields
5918
+ .filter((field) => !visibleFields.includes(field))
5919
+ .flatMap((field) => readSubmittedTextValueErrors(field, formData[field.fieldKey], {
5920
+ shapeOnly: true,
5921
+ testPattern,
5922
+ })),
5923
+ ],
5663
5924
  // A `DYNAMIC_FORM` resolver reads a member value whether or not its field
5664
5925
  // is visible, so a hidden field still holding a value must be well-formed
5665
5926
  // too. Whether a hidden field is *required* stays a visibility question
5666
5927
  // above.
5667
5928
  readMemberErrors: () => (0, form_member_value_validator_1.readSubmittedMemberValueErrors)(schema.fields.filter((field) => visibleFields.includes(field) ||
5668
5929
  isSubmittedFieldValuePresent(formData[field.fieldKey])), formData),
5930
+ // A `FORM_ORG_UNIT_MANAGER` resolver reads the unit whether or not its
5931
+ // field is visible, so a hidden field still holding a value is checked too.
5932
+ readOrgUnitErrors: () => (0, form_org_unit_value_validator_1.readSubmittedOrgUnitValueErrors)(schema.fields.filter((field) => visibleFields.includes(field) ||
5933
+ isSubmittedFieldValuePresent(formData[field.fieldKey])), formData),
5669
5934
  readTableErrors: () => [
5670
5935
  ...visibleFields
5671
5936
  .filter(form_1.isTableFieldDefinition)
5672
- .flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], isSubmittedFormFieldRequired(field, schema.fields, formData), deferredFieldKeys.has(field.fieldKey))),
5937
+ .flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], isSubmittedFormFieldRequired(field, schema.fields, formData), testPattern, deferredFieldKeys.has(field.fieldKey))),
5673
5938
  // A hidden table is not "visible", but its stored value still has to be
5674
5939
  // a well-formed table under the row cap: only the required / minimum
5675
5940
  // checks are skipped, since nobody can fill the table in.
5676
5941
  ...schema.fields
5677
5942
  .filter(form_1.isTableFieldDefinition)
5678
5943
  .filter((field) => field.hidden === true)
5679
- .flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], false, true)),
5944
+ .flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], false, testPattern, true)),
5680
5945
  ],
5681
5946
  });
5682
5947
  }
5683
5948
  /**
5684
5949
  * What an approval at a node has to leave behind: every field the node lets the
5685
5950
  * approver fill in is well-formed (a table is an array of well-formed rows with
5686
- * its required columns and minimum rows, a member value is a member id) and
5951
+ * its required columns and minimum rows, a member value is a member id, an
5952
+ * org unit value is an organization unit id) and
5687
5953
  * every one the node marks `required` is present. The messages are the submit
5688
5954
  * ones: a host tells the two entry points apart by which call it made.
5689
5955
  */
5690
5956
  function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys) {
5691
5957
  const fields = schema.fields.filter((field) => fieldKeys.has(field.fieldKey));
5958
+ const testPattern = (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)();
5692
5959
  // `readRowCellValue` is used below as an own-property reader for top-level
5693
5960
  // form keys: a key like `constructor` must not read off the prototype chain.
5694
5961
  throwFirstFormDataFieldError({
@@ -5696,27 +5963,39 @@ function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys
5696
5963
  missingFields: fields.filter((field) => !(0, form_1.isTableFieldDefinition)(field) &&
5697
5964
  requiredFieldKeys.has(field.fieldKey) &&
5698
5965
  (0, bpm_core_shared_1.isFormDataValueEmpty)(readRowCellValue(formData, field.fieldKey))),
5966
+ readTextErrors: () => fields.flatMap((field) => readSubmittedTextValueErrors(field, readRowCellValue(formData, field.fieldKey), { testPattern })),
5699
5967
  readMemberErrors: () => (0, form_member_value_validator_1.readSubmittedMemberValueErrors)(fields, formData),
5968
+ readOrgUnitErrors: () => (0, form_org_unit_value_validator_1.readSubmittedOrgUnitValueErrors)(fields, formData),
5700
5969
  readTableErrors: () => fields
5701
5970
  .filter(form_1.isTableFieldDefinition)
5702
- .flatMap((field) => validateSubmittedTableValue(field, readRowCellValue(formData, field.fieldKey), requiredFieldKeys.has(field.fieldKey))),
5971
+ .flatMap((field) => validateSubmittedTableValue(field, readRowCellValue(formData, field.fieldKey), requiredFieldKeys.has(field.fieldKey), testPattern)),
5703
5972
  });
5704
5973
  }
5705
5974
  /**
5706
5975
  * The shared tail of submit and approver-fill validation: a missing required
5707
- * field wins over a malformed member value, which wins over a table problem,
5708
- * and the later checks are only evaluated when the earlier ones pass.
5976
+ * field wins over a text value breaking its length or format rule, which wins
5977
+ * over a malformed member value, which wins over a malformed org unit value,
5978
+ * which wins over a table problem, and the later checks are only evaluated
5979
+ * when the earlier ones pass.
5709
5980
  */
5710
- function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTableErrors, }) {
5981
+ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readOrgUnitErrors, readTableErrors, readTextErrors, }) {
5711
5982
  if (missingFields.length > 0) {
5712
5983
  throw new common_1.BadRequestException(`Form data is missing required fields: ${missingFields
5713
5984
  .map((field) => field.label || field.fieldKey)
5714
5985
  .join(', ')}`);
5715
5986
  }
5987
+ const textErrors = readTextErrors();
5988
+ if (textErrors.length > 0) {
5989
+ throw new common_1.BadRequestException(textErrors.join('; '));
5990
+ }
5716
5991
  const memberErrors = readMemberErrors();
5717
5992
  if (memberErrors.length > 0) {
5718
5993
  throw new common_1.BadRequestException(memberErrors.join('; '));
5719
5994
  }
5995
+ const orgUnitErrors = readOrgUnitErrors();
5996
+ if (orgUnitErrors.length > 0) {
5997
+ throw new common_1.BadRequestException(orgUnitErrors.join('; '));
5998
+ }
5720
5999
  const tableErrors = readTableErrors();
5721
6000
  if (tableErrors.length > 0) {
5722
6001
  throw new common_1.BadRequestException(tableErrors.join('; '));
@@ -5724,11 +6003,10 @@ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTab
5724
6003
  }
5725
6004
  /**
5726
6005
  * Authoritative shape, row-count and per-row required checks for one table
5727
- * value (ADR 16 §3.7). Depth stops here on purpose: like the flat fields, only
5728
- * required / shape / row count are enforced server side; `minimum`, `maxLength`
5729
- * and friends stay a frontend hint.
6006
+ * value (ADR 16 §3.7), plus the length and format rules of text columns, which
6007
+ * the flat text fields share. `minimum` / `maximum` stay a frontend hint.
5730
6008
  */
5731
- function validateSubmittedTableValue(field, value, required, shapeOnly = false) {
6009
+ function validateSubmittedTableValue(field, value, required, testPattern, shapeOnly = false) {
5732
6010
  const path = `formData.${field.fieldKey}`;
5733
6011
  if (typeof value === 'undefined' || value === null) {
5734
6012
  return shapeOnly
@@ -5740,31 +6018,32 @@ function validateSubmittedTableValue(field, value, required, shapeOnly = false)
5740
6018
  if (!Array.isArray(value)) {
5741
6019
  return [`${path} must be an array of rows`];
5742
6020
  }
5743
- const rowErrors = value.flatMap((row, index) => validateSubmittedTableRow(field, row, `${path}[${index}]`, shapeOnly));
5744
- // A malformed row cannot be counted against min/max meaningfully; report the
5745
- // shape problem first so the submitter fixes the real cause.
6021
+ // Too many rows is refused before any row is checked: the rows a submitter
6022
+ // sends would otherwise decide how many cells the text patterns run on.
6023
+ const maxRows = readTableMaxRows(field);
6024
+ if (value.length > maxRows) {
6025
+ return [`${path} must include at most ${maxRows} row(s)`];
6026
+ }
6027
+ const rowErrors = value.flatMap((row, index) => validateSubmittedTableRow(field, row, `${path}[${index}]`, testPattern, shapeOnly));
6028
+ // A malformed row cannot be counted against the minimum meaningfully; report
6029
+ // the shape problem first so the submitter fixes the real cause.
5746
6030
  if (rowErrors.length > 0) {
5747
6031
  return rowErrors;
5748
6032
  }
5749
6033
  return validateSubmittedTableRowCount(field, value.length, required, path, shapeOnly);
5750
6034
  }
6035
+ /** The minimum only: the maximum is refused before any row is checked. */
5751
6036
  function validateSubmittedTableRowCount(field, rowCount, required, path, shapeOnly = false) {
5752
6037
  const minRows = shapeOnly
5753
6038
  ? 0
5754
6039
  : required
5755
6040
  ? Math.max(readTableMinRows(field), 1)
5756
6041
  : readTableMinRows(field);
5757
- const maxRows = readTableMaxRows(field);
5758
- return [
5759
- ...(rowCount < minRows
5760
- ? [`${path} must include at least ${minRows} row(s)`]
5761
- : []),
5762
- ...(rowCount > maxRows
5763
- ? [`${path} must include at most ${maxRows} row(s)`]
5764
- : []),
5765
- ];
5766
- }
5767
- function validateSubmittedTableRow(field, row, path, shapeOnly = false) {
6042
+ return rowCount < minRows
6043
+ ? [`${path} must include at least ${minRows} row(s)`]
6044
+ : [];
6045
+ }
6046
+ function validateSubmittedTableRow(field, row, path, testPattern, shapeOnly = false) {
5768
6047
  if (!isRecord(row)) {
5769
6048
  return [`${path} must be an object`];
5770
6049
  }
@@ -5781,7 +6060,61 @@ function validateSubmittedTableRow(field, row, path, shapeOnly = false) {
5781
6060
  .filter((column) => isSubmittedTableColumnRequired(column) &&
5782
6061
  !isSubmittedFieldValuePresent(readRowCellValue(row, column.fieldKey)))
5783
6062
  .map((column) => `${path}.${column.fieldKey} is required`);
5784
- return [...unknownKeyErrors, ...cellErrors, ...requiredErrors];
6063
+ // A shape-only pass covers a table nobody fills in, so the filler-facing
6064
+ // rules do not apply to it, the same as the required check above; only a
6065
+ // cell of the wrong type is still refused.
6066
+ const textErrors = field.columns.flatMap((column) => readSubmittedTextValueErrors(column, readRowCellValue(row, column.fieldKey), {
6067
+ path: `${path}.${column.fieldKey}`,
6068
+ shapeOnly,
6069
+ testPattern,
6070
+ }));
6071
+ return [...unknownKeyErrors, ...cellErrors, ...requiredErrors, ...textErrors];
6072
+ }
6073
+ /**
6074
+ * The server half of the text length and format rules: the same
6075
+ * `readTextFieldValueIssue` the renderer runs before it lets the filler send,
6076
+ * so a crafted request cannot store what the form would have refused — a
6077
+ * number or an array sent for a text field with rules included. `shapeOnly`
6078
+ * keeps only that type check, for a value nobody fills in at this step, and
6079
+ * does not run the pattern at all. `testPattern` is the validation pass's
6080
+ * timed pattern test (see `createTimedTextFieldPatternTest`); once its time
6081
+ * budget is spent, a value is reported as not checked rather than as a
6082
+ * format error.
6083
+ */
6084
+ function readSubmittedTextValueErrors(field, value, { path = `formData.${field.fieldKey}`, shapeOnly = false, testPattern, }) {
6085
+ if (field.type !== 'text' && field.type !== 'textarea') {
6086
+ return [];
6087
+ }
6088
+ const readIssue = () => {
6089
+ try {
6090
+ return (0, form_1.readTextFieldValueIssue)(field, value, {
6091
+ testPattern: shapeOnly ? () => true : testPattern,
6092
+ });
6093
+ }
6094
+ catch (error) {
6095
+ if (error instanceof text_field_pattern_test_1.TextFieldPatternTimeoutError) {
6096
+ return 'timeout';
6097
+ }
6098
+ throw error;
6099
+ }
6100
+ };
6101
+ const issue = readIssue();
6102
+ if (issue === 'timeout') {
6103
+ return [`${path} could not be checked against the required format in time`];
6104
+ }
6105
+ if (!issue || (shapeOnly && issue.kind !== 'type')) {
6106
+ return [];
6107
+ }
6108
+ if (issue.kind === 'type') {
6109
+ return [`${path} must be text`];
6110
+ }
6111
+ if (issue.kind === 'minLength') {
6112
+ return [`${path} must be at least ${issue.limit} characters`];
6113
+ }
6114
+ if (issue.kind === 'maxLength') {
6115
+ return [`${path} must be at most ${issue.limit} characters`];
6116
+ }
6117
+ return [`${path} does not match the required format`];
5785
6118
  }
5786
6119
  /**
5787
6120
  * Own-property read. `constructor`, `toString` and friends all satisfy the
@@ -6015,6 +6348,18 @@ function readDryRunPlaceholderAssignee(resolver) {
6015
6348
  if (resolver.type === 'ORG_UNIT_MANAGER') {
6016
6349
  return `orgUnitManager:${resolver.orgUnitId}`;
6017
6350
  }
6351
+ if (resolver.type === 'FORM_MEMBER_MANAGER') {
6352
+ return `formMemberManager:${resolver.formPath}:${resolver.levelsUp}`;
6353
+ }
6354
+ if (resolver.type === 'FORM_ORG_UNIT_MANAGER') {
6355
+ return `formOrgUnitManager:${resolver.formPath}`;
6356
+ }
6357
+ if (resolver.type === 'NODE_ACTOR') {
6358
+ return `nodeActor:${resolver.nodeIds.join(',')}`;
6359
+ }
6360
+ if (resolver.type === 'NODE_ACTOR_MANAGER') {
6361
+ return `nodeActorManager:${resolver.nodeIds.join(',')}:${resolver.levelsUp}`;
6362
+ }
6018
6363
  return resolver.type === 'ORG_MANAGER_CHAIN'
6019
6364
  ? `orgManagerChain:${resolver.minPositionLevel}`
6020
6365
  : resolver.type;