@rytass/bpm-core-nestjs-module 0.18.0 → 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 (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +2 -2
  3. package/src/lib/form/form-org-unit-value.validator.d.ts +21 -0
  4. package/src/lib/form/form-org-unit-value.validator.js +43 -0
  5. package/src/lib/form/form-org-unit-value.validator.js.map +1 -0
  6. package/src/lib/form/form-schema.validator.js +17 -0
  7. package/src/lib/form/form-schema.validator.js.map +1 -1
  8. package/src/lib/notification/notification.service.js +3 -1
  9. package/src/lib/notification/notification.service.js.map +1 -1
  10. package/src/lib/organization/dto/org-unit-option.object.d.ts +13 -0
  11. package/src/lib/organization/dto/org-unit-option.object.js +35 -0
  12. package/src/lib/organization/dto/org-unit-option.object.js.map +1 -0
  13. package/src/lib/organization/index.d.ts +2 -0
  14. package/src/lib/organization/index.js +2 -0
  15. package/src/lib/organization/index.js.map +1 -1
  16. package/src/lib/organization/organization-picker.queries.d.ts +17 -0
  17. package/src/lib/organization/organization-picker.queries.js +50 -0
  18. package/src/lib/organization/organization-picker.queries.js.map +1 -0
  19. package/src/lib/organization/organization.module.js +2 -0
  20. package/src/lib/organization/organization.module.js.map +1 -1
  21. package/src/lib/organization/organization.service.d.ts +17 -0
  22. package/src/lib/organization/organization.service.js +61 -0
  23. package/src/lib/organization/organization.service.js.map +1 -1
  24. package/src/lib/template/template.service.js +73 -4
  25. package/src/lib/template/template.service.js.map +1 -1
  26. package/src/lib/template/workflow-definition.validator.js +58 -8
  27. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  28. package/src/lib/workflow-engine/workflow-engine.service.d.ts +59 -0
  29. package/src/lib/workflow-engine/workflow-engine.service.js +241 -15
  30. 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");
@@ -682,6 +683,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
682
683
  decisionId: decision.id,
683
684
  signatureId: signature.id,
684
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 } : {}),
685
689
  // Keys only: this payload is forwarded to hosts, and the values
686
690
  // can be tax IDs or amounts.
687
691
  ...(changedFieldKeys.length > 0 ? { changedFieldKeys } : {}),
@@ -1826,6 +1830,16 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1826
1830
  resolveDryRunAssignment(input, node) {
1827
1831
  const resolver = node.data.approverResolver;
1828
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
+ }
1829
1843
  const memberIds = this.readDryRunMemberIds(input, resolver, `workflow.nodes.${node.id}.data.approverResolver`);
1830
1844
  if (memberIds === null) {
1831
1845
  return {
@@ -1911,6 +1925,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
1911
1925
  ? managerMemberIds
1912
1926
  : readDryRunFallbackMemberIds(input, resolver.fallback);
1913
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.
1914
1930
  return null;
1915
1931
  }
1916
1932
  async getApprovalInstance(id, scope) {
@@ -3906,31 +3922,175 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
3906
3922
  : null;
3907
3923
  }
3908
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) {
3909
3933
  if (resolver.type === 'DIRECT') {
3910
- return readMemberIdsFromValue(resolver.memberIds, label, subject).map((memberId) => ({ memberId, sourceType: resolver.type }));
3934
+ return readMemberIdsFromValue(resolver.memberIds, label, subject);
3911
3935
  }
3912
3936
  if (resolver.type === 'DYNAMIC_FORM') {
3913
- 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}」`);
3914
3938
  }
3915
3939
  if (resolver.type === 'EXPRESSION') {
3916
- 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, '依運算式');
3917
3941
  }
3918
3942
  if (resolver.type === 'POSITION') {
3919
- 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);
3920
3944
  }
3921
3945
  if (resolver.type === 'ORG_UNIT_MEMBER') {
3922
- 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);
3923
3947
  }
3924
3948
  if (resolver.type === 'ORG_UNIT_POSITION') {
3925
- 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);
3926
3950
  }
3927
3951
  if (resolver.type === 'ORG_UNIT_MANAGER') {
3928
- 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);
3929
3953
  }
3930
3954
  if (resolver.type === 'ORG_MANAGER_CHAIN') {
3931
- 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}」找不到所選組織`);
4012
+ }
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 [];
3932
4065
  }
3933
- return this.resolveInitiatorManagerCandidates(manager, instance, resolver.levelsUp, resolver.fallback, label, resolver.preferClosestOrgUnit === true, subject).then((memberIds) => memberIds.map((memberId) => ({ memberId, sourceType: resolver.type })));
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));
3934
4094
  }
3935
4095
  async resolvePositionCandidates(manager, positionId, label, subject) {
3936
4096
  const memberIds = await this.readActivePositionMemberIds(manager, positionId);
@@ -4115,6 +4275,29 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4115
4275
  const nextLevelGroups = await Promise.all(directManagerMemberIds.map((managerMemberId) => this.resolveManagerMemberIdsAtLevel(manager, managerMemberId, date, remainingLevels - 1, preferClosestOrgUnit)));
4116
4276
  return uniqueTexts(nextLevelGroups.flat());
4117
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
+ }
4118
4301
  async resolveDirectManagerMemberIds(manager, memberId, date, preferClosestOrgUnit, excludedManagerMemberIds = []) {
4119
4302
  const memberships = await manager.getRepository(membership_entity_1.MembershipEntity).find({
4120
4303
  where: { memberId },
@@ -4338,6 +4521,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
4338
4521
  await this.advanceTokenToOutgoingNodes(manager, updatedInstance, token, node);
4339
4522
  return;
4340
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
+ }
4341
4532
  // A webhook-only NOTIFY node names nobody. Resolving its empty DIRECT
4342
4533
  // resolver would throw and roll back the submit or decision that reached
4343
4534
  // it, so skip member resolution; webhooks are delivered separately.
@@ -4983,6 +5174,17 @@ function readWorkflowNodeLabel(workflow, nodeId) {
4983
5174
  ?.data.label;
4984
5175
  return typeof label === 'string' && label.trim() ? label : null;
4985
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
+ }
4986
5188
  function readWorkflowNodeOrThrow(workflow, nodeId) {
4987
5189
  const node = workflow.nodes.find((candidate) => candidate.id === nodeId);
4988
5190
  if (!node) {
@@ -5683,8 +5885,9 @@ function readCandidateStatusForClosedTask(taskStatus) {
5683
5885
  /**
5684
5886
  * `deferredFieldKeys` are fields an approver fills in later: they are checked
5685
5887
  * for shape (a table is an array of well-formed rows, a member value is a
5686
- * member id) but never for being present, so an empty one cannot block a
5687
- * 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.
5688
5891
  */
5689
5892
  function validateSubmittedFormData(schema, formData, options = {}) {
5690
5893
  const deferredFieldKeys = options.deferredFieldKeys ?? new Set();
@@ -5724,6 +5927,10 @@ function validateSubmittedFormData(schema, formData, options = {}) {
5724
5927
  // above.
5725
5928
  readMemberErrors: () => (0, form_member_value_validator_1.readSubmittedMemberValueErrors)(schema.fields.filter((field) => visibleFields.includes(field) ||
5726
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),
5727
5934
  readTableErrors: () => [
5728
5935
  ...visibleFields
5729
5936
  .filter(form_1.isTableFieldDefinition)
@@ -5741,7 +5948,8 @@ function validateSubmittedFormData(schema, formData, options = {}) {
5741
5948
  /**
5742
5949
  * What an approval at a node has to leave behind: every field the node lets the
5743
5950
  * approver fill in is well-formed (a table is an array of well-formed rows with
5744
- * 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
5745
5953
  * every one the node marks `required` is present. The messages are the submit
5746
5954
  * ones: a host tells the two entry points apart by which call it made.
5747
5955
  */
@@ -5757,6 +5965,7 @@ function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys
5757
5965
  (0, bpm_core_shared_1.isFormDataValueEmpty)(readRowCellValue(formData, field.fieldKey))),
5758
5966
  readTextErrors: () => fields.flatMap((field) => readSubmittedTextValueErrors(field, readRowCellValue(formData, field.fieldKey), { testPattern })),
5759
5967
  readMemberErrors: () => (0, form_member_value_validator_1.readSubmittedMemberValueErrors)(fields, formData),
5968
+ readOrgUnitErrors: () => (0, form_org_unit_value_validator_1.readSubmittedOrgUnitValueErrors)(fields, formData),
5760
5969
  readTableErrors: () => fields
5761
5970
  .filter(form_1.isTableFieldDefinition)
5762
5971
  .flatMap((field) => validateSubmittedTableValue(field, readRowCellValue(formData, field.fieldKey), requiredFieldKeys.has(field.fieldKey), testPattern)),
@@ -5765,10 +5974,11 @@ function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys
5765
5974
  /**
5766
5975
  * The shared tail of submit and approver-fill validation: a missing required
5767
5976
  * field wins over a text value breaking its length or format rule, which wins
5768
- * over a malformed member value, which wins over a table problem, and the
5769
- * later checks are only evaluated when the earlier ones pass.
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.
5770
5980
  */
5771
- function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTableErrors, readTextErrors, }) {
5981
+ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readOrgUnitErrors, readTableErrors, readTextErrors, }) {
5772
5982
  if (missingFields.length > 0) {
5773
5983
  throw new common_1.BadRequestException(`Form data is missing required fields: ${missingFields
5774
5984
  .map((field) => field.label || field.fieldKey)
@@ -5782,6 +5992,10 @@ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTab
5782
5992
  if (memberErrors.length > 0) {
5783
5993
  throw new common_1.BadRequestException(memberErrors.join('; '));
5784
5994
  }
5995
+ const orgUnitErrors = readOrgUnitErrors();
5996
+ if (orgUnitErrors.length > 0) {
5997
+ throw new common_1.BadRequestException(orgUnitErrors.join('; '));
5998
+ }
5785
5999
  const tableErrors = readTableErrors();
5786
6000
  if (tableErrors.length > 0) {
5787
6001
  throw new common_1.BadRequestException(tableErrors.join('; '));
@@ -6134,6 +6348,18 @@ function readDryRunPlaceholderAssignee(resolver) {
6134
6348
  if (resolver.type === 'ORG_UNIT_MANAGER') {
6135
6349
  return `orgUnitManager:${resolver.orgUnitId}`;
6136
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
+ }
6137
6363
  return resolver.type === 'ORG_MANAGER_CHAIN'
6138
6364
  ? `orgManagerChain:${resolver.minPositionLevel}`
6139
6365
  : resolver.type;