@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.
- package/CHANGELOG.md +68 -0
- package/README.md +29 -2
- package/package.json +2 -2
- package/src/lib/delegation/delegation.service.d.ts +41 -1
- package/src/lib/delegation/delegation.service.js +147 -23
- package/src/lib/delegation/delegation.service.js.map +1 -1
- package/src/lib/form/form-org-unit-value.validator.d.ts +21 -0
- package/src/lib/form/form-org-unit-value.validator.js +43 -0
- package/src/lib/form/form-org-unit-value.validator.js.map +1 -0
- package/src/lib/form/form-schema.validator.js +83 -6
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/notification/notification.service.js +3 -1
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/organization/dto/org-unit-option.object.d.ts +13 -0
- package/src/lib/organization/dto/org-unit-option.object.js +35 -0
- package/src/lib/organization/dto/org-unit-option.object.js.map +1 -0
- package/src/lib/organization/index.d.ts +2 -0
- package/src/lib/organization/index.js +2 -0
- package/src/lib/organization/index.js.map +1 -1
- package/src/lib/organization/organization-picker.queries.d.ts +17 -0
- package/src/lib/organization/organization-picker.queries.js +50 -0
- package/src/lib/organization/organization-picker.queries.js.map +1 -0
- package/src/lib/organization/organization.module.js +2 -0
- package/src/lib/organization/organization.module.js.map +1 -1
- package/src/lib/organization/organization.service.d.ts +17 -0
- package/src/lib/organization/organization.service.js +61 -0
- package/src/lib/organization/organization.service.js.map +1 -1
- package/src/lib/template/template.service.js +73 -4
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/template/workflow-definition.validator.js +58 -8
- package/src/lib/template/workflow-definition.validator.js.map +1 -1
- package/src/lib/workflow-engine/text-field-pattern-test.d.ts +23 -0
- package/src/lib/workflow-engine/text-field-pattern-test.js +75 -0
- package/src/lib/workflow-engine/text-field-pattern-test.js.map +1 -0
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +59 -0
- package/src/lib/workflow-engine/workflow-engine.service.js +386 -41
- 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
|
-
|
|
3844
|
-
|
|
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:
|
|
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)
|
|
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}」`)
|
|
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, '依運算式')
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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.
|
|
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
|
|
5649
|
-
* submission that has nobody to
|
|
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
|
|
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
|
|
5708
|
-
*
|
|
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)
|
|
5728
|
-
*
|
|
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
|
-
|
|
5744
|
-
//
|
|
5745
|
-
|
|
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
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
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
|
-
|
|
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;
|