@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.
- package/CHANGELOG.md +15 -0
- package/package.json +2 -2
- 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 +17 -0
- 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/workflow-engine.service.d.ts +59 -0
- package/src/lib/workflow-engine/workflow-engine.service.js +241 -15
- 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)
|
|
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}」`)
|
|
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, '依運算式')
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
5687
|
-
* 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.
|
|
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
|
|
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
|
|
5769
|
-
*
|
|
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;
|