@rytass/bpm-core-nestjs-module 0.17.8 → 0.18.0
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 +73 -0
- package/README.md +40 -0
- 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-schema.validator.js +66 -6
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/identity/identity.service.d.ts +6 -0
- package/src/lib/identity/identity.service.js +12 -0
- package/src/lib/identity/identity.service.js.map +1 -1
- package/src/lib/identity/member-base.adapter.d.ts +8 -0
- package/src/lib/identity/member-base.adapter.js +8 -2
- package/src/lib/identity/member-base.adapter.js.map +1 -1
- package/src/lib/identity/member-resolver.interface.d.ts +9 -0
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.d.ts +7 -0
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js +7 -0
- package/src/lib/workflow-engine/dto/resubmit-approval-instance.input.js.map +1 -1
- package/src/lib/workflow-engine/task-reassignment.object.d.ts +11 -0
- package/src/lib/workflow-engine/task-reassignment.object.js +27 -1
- package/src/lib/workflow-engine/task-reassignment.object.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.queries.d.ts +12 -2
- package/src/lib/workflow-engine/workflow-engine.queries.js +45 -2
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +32 -0
- package/src/lib/workflow-engine/workflow-engine.service.js +253 -29
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkflowEngineQueries = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
+
const core_1 = require("@nestjs/core");
|
|
6
|
+
const unknown_element_exception_js_1 = require("@nestjs/core/errors/exceptions/unknown-element.exception.js");
|
|
5
7
|
const graphql_1 = require("@nestjs/graphql");
|
|
6
8
|
const bpm_auth_1 = require("../bpm-auth");
|
|
9
|
+
const identity_service_1 = require("../identity/identity.service");
|
|
7
10
|
const task_reassignment_object_1 = require("./task-reassignment.object");
|
|
8
11
|
const activity_log_entity_1 = require("./activity-log.entity");
|
|
9
12
|
const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
|
|
@@ -18,8 +21,9 @@ const workflow_engine_enums_1 = require("./workflow-engine.enums");
|
|
|
18
21
|
const workflow_engine_service_1 = require("./workflow-engine.service");
|
|
19
22
|
const workflow_token_entity_1 = require("./workflow-token.entity");
|
|
20
23
|
let WorkflowEngineQueries = class WorkflowEngineQueries {
|
|
21
|
-
constructor(workflowEngineService) {
|
|
24
|
+
constructor(workflowEngineService, moduleRef) {
|
|
22
25
|
this.workflowEngineService = workflowEngineService;
|
|
26
|
+
this.moduleRef = moduleRef;
|
|
23
27
|
}
|
|
24
28
|
async approvalInstances(view, searchText, state, templateId, page, pageSize, currentAuthContext) {
|
|
25
29
|
return this.workflowEngineService.listApprovalInstances(currentAuthContext, {
|
|
@@ -73,6 +77,13 @@ let WorkflowEngineQueries = class WorkflowEngineQueries {
|
|
|
73
77
|
async memberOpenTasks(memberId) {
|
|
74
78
|
return this.workflowEngineService.listMemberOpenTasks(memberId);
|
|
75
79
|
}
|
|
80
|
+
async inactiveMembersWithOpenTasks() {
|
|
81
|
+
const identityService = this.readIdentityService();
|
|
82
|
+
if (!identityService) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return this.workflowEngineService.listInactiveMembersWithOpenTasks((memberIds) => identityService.resolveInactiveMembers(memberIds));
|
|
86
|
+
}
|
|
76
87
|
async launchableApprovalTemplates(currentMemberId) {
|
|
77
88
|
return this.workflowEngineService.listLaunchableApprovalTemplates(currentMemberId);
|
|
78
89
|
}
|
|
@@ -88,6 +99,25 @@ let WorkflowEngineQueries = class WorkflowEngineQueries {
|
|
|
88
99
|
async activityLogs(instanceId, currentAuthContext) {
|
|
89
100
|
return this.workflowEngineService.listActivityLogs(instanceId, currentAuthContext);
|
|
90
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Looked up at call time rather than injected: `IdentityModule` is a dynamic
|
|
104
|
+
* module the root configures with the host's options, so this module cannot
|
|
105
|
+
* import it. A host that wires the engine without it gets an empty list, the
|
|
106
|
+
* same answer as a member resolver without `resolveInactive`.
|
|
107
|
+
*/
|
|
108
|
+
readIdentityService() {
|
|
109
|
+
try {
|
|
110
|
+
return this.moduleRef.get(identity_service_1.IdentityService, { strict: false });
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// Only "not registered" means no identity module; anything else is a
|
|
114
|
+
// real failure and must not pass for an empty list.
|
|
115
|
+
if (error instanceof unknown_element_exception_js_1.UnknownElementException) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
91
121
|
};
|
|
92
122
|
exports.WorkflowEngineQueries = WorkflowEngineQueries;
|
|
93
123
|
tslib_1.__decorate([
|
|
@@ -205,6 +235,18 @@ tslib_1.__decorate([
|
|
|
205
235
|
tslib_1.__metadata("design:paramtypes", [String]),
|
|
206
236
|
tslib_1.__metadata("design:returntype", Promise)
|
|
207
237
|
], WorkflowEngineQueries.prototype, "memberOpenTasks", null);
|
|
238
|
+
tslib_1.__decorate([
|
|
239
|
+
(0, graphql_1.Query)(() => [task_reassignment_object_1.InactiveMemberWithOpenTasksObject], {
|
|
240
|
+
description: 'Administrator: members the host reports as deactivated who still hold ' +
|
|
241
|
+
'open tasks, with the count memberOpenTasks would list. Always empty ' +
|
|
242
|
+
'when the host member resolver does not implement resolveInactive, or ' +
|
|
243
|
+
'when the engine is wired without the identity module.',
|
|
244
|
+
}),
|
|
245
|
+
(0, bpm_auth_1.BPMAdminOnly)(),
|
|
246
|
+
tslib_1.__metadata("design:type", Function),
|
|
247
|
+
tslib_1.__metadata("design:paramtypes", []),
|
|
248
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
249
|
+
], WorkflowEngineQueries.prototype, "inactiveMembersWithOpenTasks", null);
|
|
208
250
|
tslib_1.__decorate([
|
|
209
251
|
(0, graphql_1.Query)(() => [approval_template_entity_1.ApprovalTemplateEntity]),
|
|
210
252
|
tslib_1.__param(0, (0, bpm_auth_1.BPMCurrentMemberId)()),
|
|
@@ -247,6 +289,7 @@ tslib_1.__decorate([
|
|
|
247
289
|
exports.WorkflowEngineQueries = WorkflowEngineQueries = tslib_1.__decorate([
|
|
248
290
|
(0, graphql_1.Resolver)(),
|
|
249
291
|
(0, bpm_auth_1.BPMAuthenticated)(),
|
|
250
|
-
tslib_1.__metadata("design:paramtypes", [workflow_engine_service_1.WorkflowEngineService
|
|
292
|
+
tslib_1.__metadata("design:paramtypes", [workflow_engine_service_1.WorkflowEngineService,
|
|
293
|
+
core_1.ModuleRef])
|
|
251
294
|
], WorkflowEngineQueries);
|
|
252
295
|
//# sourceMappingURL=workflow-engine.queries.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow-engine.queries.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/workflow-engine/workflow-engine.queries.ts"],"names":[],"mappings":";;;;AAAA,6CAAiE;AACjE,0CAMqB;AACrB,
|
|
1
|
+
{"version":3,"file":"workflow-engine.queries.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/workflow-engine/workflow-engine.queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MemberMetadata } from '@rytass/bpm-core-shared';
|
|
1
2
|
import { WorkflowWebhookDeliveryService } from '../workflow-webhook/workflow-webhook-delivery.service';
|
|
2
3
|
import { Repository } from 'typeorm';
|
|
3
4
|
import { AttachmentService } from '../attachment/attachment.service';
|
|
@@ -105,6 +106,14 @@ export interface MemberOpenTaskRecord {
|
|
|
105
106
|
readonly templateId: string;
|
|
106
107
|
readonly templateName: string | null;
|
|
107
108
|
}
|
|
109
|
+
/** A deactivated member who still holds open tasks, for the reassignment screen. */
|
|
110
|
+
export interface InactiveMemberOpenTasksRecord {
|
|
111
|
+
readonly email: string;
|
|
112
|
+
readonly memberId: string;
|
|
113
|
+
readonly name: string;
|
|
114
|
+
/** Same count as {@link WorkflowEngineService.listMemberOpenTasks} returns rows. */
|
|
115
|
+
readonly openTaskCount: number;
|
|
116
|
+
}
|
|
108
117
|
/**
|
|
109
118
|
* Engine-internal decision knobs that are deliberately kept out of
|
|
110
119
|
* {@link DecideTaskInput} so they never reach the GraphQL schema.
|
|
@@ -392,6 +401,29 @@ export declare class WorkflowEngineService {
|
|
|
392
401
|
* RUNNING instances.
|
|
393
402
|
*/
|
|
394
403
|
listMemberOpenTasks(memberId: string): Promise<readonly MemberOpenTaskRecord[]>;
|
|
404
|
+
/**
|
|
405
|
+
* Deactivated members who still hold open tasks, so an administrator can find
|
|
406
|
+
* them: a deactivated member no longer shows up in the member search.
|
|
407
|
+
*
|
|
408
|
+
* BPM cannot tell a deactivated member from an id it never knew, so the host
|
|
409
|
+
* names them: `resolveInactiveMembers` gets every member id that holds an open
|
|
410
|
+
* seat and returns the ones it knows but has deactivated. The count per member
|
|
411
|
+
* is the {@link listMemberOpenTasks} count, and members left with none are
|
|
412
|
+
* dropped.
|
|
413
|
+
*/
|
|
414
|
+
listInactiveMembersWithOpenTasks(resolveInactiveMembers: (memberIds: readonly string[]) => Promise<readonly MemberMetadata[]>): Promise<readonly InactiveMemberOpenTasksRecord[]>;
|
|
415
|
+
/**
|
|
416
|
+
* Every member id that may hold an open seat on a RUNNING instance: the
|
|
417
|
+
* assignee of an open task, or an open candidate row on one. Still a
|
|
418
|
+
* superset (a countersign voter who already decided stays the assignee);
|
|
419
|
+
* callers count with {@link readRunningMemberTasks}, which drops those.
|
|
420
|
+
*
|
|
421
|
+
* One query that returns only the distinct holder ids, never a row per
|
|
422
|
+
* task or seat: the member screen loads it every time.
|
|
423
|
+
*/
|
|
424
|
+
private readOpenSeatHolderMemberIds;
|
|
425
|
+
/** `schema.table` of a repository's entity, escaped for raw SQL. */
|
|
426
|
+
private readQualifiedTableName;
|
|
395
427
|
/**
|
|
396
428
|
* The open tasks `memberId` holds on RUNNING instances, with the instance
|
|
397
429
|
* each belongs to. The instance filter is applied here, not only in the query.
|
|
@@ -38,6 +38,7 @@ const activity_log_entity_1 = require("./activity-log.entity");
|
|
|
38
38
|
const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
|
|
39
39
|
const adhoc_enums_1 = require("./adhoc.enums");
|
|
40
40
|
const approval_instance_entity_1 = require("./approval-instance.entity");
|
|
41
|
+
const text_field_pattern_test_1 = require("./text-field-pattern-test");
|
|
41
42
|
const form_data_fingerprint_1 = require("./form-data-fingerprint");
|
|
42
43
|
const instance_submitter_1 = require("./instance-submitter");
|
|
43
44
|
const task_decision_entity_1 = require("./task-decision.entity");
|
|
@@ -113,6 +114,15 @@ class TaskReassignmentRejectedError extends Error {
|
|
|
113
114
|
this.errorCode = errorCode;
|
|
114
115
|
}
|
|
115
116
|
}
|
|
117
|
+
/** The administrator screens are in Traditional Chinese: names sort by stroke count. */
|
|
118
|
+
const INACTIVE_MEMBER_NAME_LOCALE = 'zh-Hant-TW';
|
|
119
|
+
/**
|
|
120
|
+
* How many deactivated members have their open tasks counted at once. Each
|
|
121
|
+
* count is a few queries, so an unbounded fan-out would put one query in
|
|
122
|
+
* flight per member, and claim that many pool connections at the same time, on
|
|
123
|
+
* every load of the member screen.
|
|
124
|
+
*/
|
|
125
|
+
const INACTIVE_MEMBER_COUNT_CONCURRENCY = 5;
|
|
116
126
|
const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
|
|
117
127
|
'bpm:*',
|
|
118
128
|
'bpm.workflow.read_all',
|
|
@@ -908,6 +918,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
908
918
|
instanceId: instance.id,
|
|
909
919
|
nodeId: null,
|
|
910
920
|
payload: {
|
|
921
|
+
comment: input.comment?.trim() || null,
|
|
911
922
|
resubmitStrategy: returnedContext?.resubmitStrategy ?? 'RESTART',
|
|
912
923
|
},
|
|
913
924
|
taskId: null,
|
|
@@ -3340,6 +3351,97 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3340
3351
|
};
|
|
3341
3352
|
});
|
|
3342
3353
|
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Deactivated members who still hold open tasks, so an administrator can find
|
|
3356
|
+
* them: a deactivated member no longer shows up in the member search.
|
|
3357
|
+
*
|
|
3358
|
+
* BPM cannot tell a deactivated member from an id it never knew, so the host
|
|
3359
|
+
* names them: `resolveInactiveMembers` gets every member id that holds an open
|
|
3360
|
+
* seat and returns the ones it knows but has deactivated. The count per member
|
|
3361
|
+
* is the {@link listMemberOpenTasks} count, and members left with none are
|
|
3362
|
+
* dropped.
|
|
3363
|
+
*/
|
|
3364
|
+
async listInactiveMembersWithOpenTasks(resolveInactiveMembers) {
|
|
3365
|
+
const holderIds = await this.readOpenSeatHolderMemberIds();
|
|
3366
|
+
if (!holderIds.length) {
|
|
3367
|
+
return [];
|
|
3368
|
+
}
|
|
3369
|
+
const holderIdSet = new Set(holderIds);
|
|
3370
|
+
// Only the ids it was asked about are counted, each once: a member the
|
|
3371
|
+
// host adds would only cost task queries before the count dropped it.
|
|
3372
|
+
const inactiveMembers = [
|
|
3373
|
+
...new Map((await resolveInactiveMembers(holderIds))
|
|
3374
|
+
.filter((member) => holderIdSet.has(member.memberId))
|
|
3375
|
+
.map((member) => [member.memberId, member])).values(),
|
|
3376
|
+
];
|
|
3377
|
+
const counted = await chunkArray(inactiveMembers, INACTIVE_MEMBER_COUNT_CONCURRENCY).reduce(async (previous, batch) => [
|
|
3378
|
+
...(await previous),
|
|
3379
|
+
...(await Promise.all(batch.map(async (member) => ({
|
|
3380
|
+
email: member.email,
|
|
3381
|
+
memberId: member.memberId,
|
|
3382
|
+
name: member.name,
|
|
3383
|
+
openTaskCount: (await this.readRunningMemberTasks(member.memberId))
|
|
3384
|
+
.length,
|
|
3385
|
+
})))),
|
|
3386
|
+
], Promise.resolve([]));
|
|
3387
|
+
return counted
|
|
3388
|
+
.filter((record) => record.openTaskCount > 0)
|
|
3389
|
+
.sort((left, right) => left.name.localeCompare(right.name, INACTIVE_MEMBER_NAME_LOCALE) ||
|
|
3390
|
+
// Namesakes are common; the id keeps their order stable.
|
|
3391
|
+
(left.memberId < right.memberId
|
|
3392
|
+
? -1
|
|
3393
|
+
: left.memberId > right.memberId
|
|
3394
|
+
? 1
|
|
3395
|
+
: 0));
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Every member id that may hold an open seat on a RUNNING instance: the
|
|
3399
|
+
* assignee of an open task, or an open candidate row on one. Still a
|
|
3400
|
+
* superset (a countersign voter who already decided stays the assignee);
|
|
3401
|
+
* callers count with {@link readRunningMemberTasks}, which drops those.
|
|
3402
|
+
*
|
|
3403
|
+
* One query that returns only the distinct holder ids, never a row per
|
|
3404
|
+
* task or seat: the member screen loads it every time.
|
|
3405
|
+
*/
|
|
3406
|
+
async readOpenSeatHolderMemberIds() {
|
|
3407
|
+
const taskTable = this.readQualifiedTableName(this.taskRepository);
|
|
3408
|
+
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
3409
|
+
const instanceTable = this.readQualifiedTableName(this.approvalInstanceRepository);
|
|
3410
|
+
const rows = await this.approvalInstanceRepository.query(`SELECT holder.member_id AS "memberId"
|
|
3411
|
+
FROM (
|
|
3412
|
+
SELECT task.assignee_member_id AS member_id
|
|
3413
|
+
FROM ${taskTable} AS task
|
|
3414
|
+
JOIN ${instanceTable} AS instance
|
|
3415
|
+
ON instance.id = task.instance_id
|
|
3416
|
+
WHERE task.status = ANY($1::text[])
|
|
3417
|
+
AND task.assignee_member_id IS NOT NULL
|
|
3418
|
+
AND instance.state = $2
|
|
3419
|
+
UNION
|
|
3420
|
+
SELECT candidate.member_id
|
|
3421
|
+
FROM ${candidateTable} AS candidate
|
|
3422
|
+
JOIN ${taskTable} AS task ON task.id = candidate.task_id
|
|
3423
|
+
JOIN ${instanceTable} AS instance
|
|
3424
|
+
ON instance.id = task.instance_id
|
|
3425
|
+
WHERE candidate.status = ANY($3::text[])
|
|
3426
|
+
AND task.status = ANY($1::text[])
|
|
3427
|
+
AND instance.state = $2
|
|
3428
|
+
) AS holder`, [
|
|
3429
|
+
[workflow_engine_enums_1.TaskStatusEnum.PENDING, workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS],
|
|
3430
|
+
workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING,
|
|
3431
|
+
[...OPEN_SEAT_STATUSES],
|
|
3432
|
+
]);
|
|
3433
|
+
// Sorted here, not in SQL: the default JS order (UTF-16 code units),
|
|
3434
|
+
// whatever the DB collation.
|
|
3435
|
+
return rows.map((row) => row.memberId).sort();
|
|
3436
|
+
}
|
|
3437
|
+
/** `schema.table` of a repository's entity, escaped for raw SQL. */
|
|
3438
|
+
readQualifiedTableName(repository) {
|
|
3439
|
+
const { driver } = this.approvalInstanceRepository.manager.connection;
|
|
3440
|
+
return [repository.metadata.schema, repository.metadata.tableName]
|
|
3441
|
+
.filter((part) => Boolean(part))
|
|
3442
|
+
.map((part) => driver.escape(part))
|
|
3443
|
+
.join('.');
|
|
3444
|
+
}
|
|
3343
3445
|
/**
|
|
3344
3446
|
* The open tasks `memberId` holds on RUNNING instances, with the instance
|
|
3345
3447
|
* each belongs to. The instance filter is applied here, not only in the query.
|
|
@@ -3739,11 +3841,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3739
3841
|
// Not deduplicated: a node that excludes the initiator filters first, so a
|
|
3740
3842
|
// first-seen entry that must be dropped cannot shadow a legitimate one.
|
|
3741
3843
|
async delegateResolvedCandidates(manager, instance, nodeId, resolvedCandidates, gradeGated) {
|
|
3742
|
-
|
|
3743
|
-
|
|
3844
|
+
// One batch for the node, so the host directory is asked once for every
|
|
3845
|
+
// candidate's delegation agents rather than once per candidate.
|
|
3846
|
+
const resolutions = await this.delegationService.resolveAssignees(resolvedCandidates.map((candidate) => candidate.memberId), this.buildDelegationContext(instance, nodeId));
|
|
3847
|
+
const resolved = resolvedCandidates.map((candidate, index) => ({
|
|
3744
3848
|
candidate,
|
|
3745
|
-
resolution:
|
|
3746
|
-
}))
|
|
3849
|
+
resolution: resolutions[index],
|
|
3850
|
+
}));
|
|
3747
3851
|
// One batch of grade lookups for the whole node, not two per delegate.
|
|
3748
3852
|
const levels = gradeGated
|
|
3749
3853
|
? await this.readMemberPositionLevels(manager, resolved.flatMap(({ candidate, resolution }) => readGradeGateMemberIds(candidate.memberId, resolution)), this.readOrganizationToday())
|
|
@@ -4200,6 +4304,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4200
4304
|
}
|
|
4201
4305
|
if (action.type === 'SET_FORM_FIELD') {
|
|
4202
4306
|
const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`);
|
|
4307
|
+
assertServiceTaskTextValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
|
|
4203
4308
|
const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
|
|
4204
4309
|
// A service task runs inside this transaction, so it cannot call a host
|
|
4205
4310
|
// provider to re-resolve the label. Rather than leave the previous
|
|
@@ -4651,6 +4756,34 @@ function readDataSourceFieldKeyAtPath(snapshot, fieldPath) {
|
|
|
4651
4756
|
const field = schema.fields.find((candidate) => isRecord(candidate) && candidate.fieldKey === fieldKey);
|
|
4652
4757
|
return field && (0, form_1.isFormDataSourceFieldDefinition)(field) ? field.fieldKey : null;
|
|
4653
4758
|
}
|
|
4759
|
+
/**
|
|
4760
|
+
* Holds a `SET_FORM_FIELD` write to the length and format rules the form
|
|
4761
|
+
* applies to a filler, so a service task cannot store text the field would
|
|
4762
|
+
* have refused. Breaking them throws, which rolls back the submit or decision
|
|
4763
|
+
* that reached the node: the value is never half-written, and the node's value
|
|
4764
|
+
* has to be corrected. Only a top-level field can carry those rules — the
|
|
4765
|
+
* writer ignores an indexed path, and a nested record path names no field —
|
|
4766
|
+
* and an unreadable snapshot is left alone, as in
|
|
4767
|
+
* `readDataSourceFieldKeyAtPath`.
|
|
4768
|
+
*/
|
|
4769
|
+
function assertServiceTaskTextValue(snapshot, fieldPath, value, nodeLabel) {
|
|
4770
|
+
const schema = snapshot?.schema;
|
|
4771
|
+
const segments = normalizeFormFieldPath(fieldPath);
|
|
4772
|
+
if (!isFormDefinitionSchema(schema) || segments.length !== 1) {
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4775
|
+
const field = schema.fields.find((candidate) => candidate.fieldKey === segments[0]);
|
|
4776
|
+
const errors = field
|
|
4777
|
+
? readSubmittedTextValueErrors(field, value, {
|
|
4778
|
+
testPattern: (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)(),
|
|
4779
|
+
})
|
|
4780
|
+
: [];
|
|
4781
|
+
if (!field || errors.length === 0) {
|
|
4782
|
+
return;
|
|
4783
|
+
}
|
|
4784
|
+
throw new common_1.BadRequestException(`系統節點「${nodeLabel}」要寫入欄位「${field.label || field.fieldKey}」的值不符合該欄位的規則` +
|
|
4785
|
+
`(${errors.join('; ')}),這次操作沒有生效;請聯絡流程管理者修正此節點的寫入值`);
|
|
4786
|
+
}
|
|
4654
4787
|
function readSnapshotWithoutField(snapshots, fieldKey) {
|
|
4655
4788
|
return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => key !== fieldKey));
|
|
4656
4789
|
}
|
|
@@ -4897,6 +5030,12 @@ function readReturnTargetNodeId(workflow, currentNodeId, requestedNodeId) {
|
|
|
4897
5030
|
throw new common_1.BadRequestException('returnToNodeId is required');
|
|
4898
5031
|
}
|
|
4899
5032
|
readWorkflowNodeOrThrow(workflow, trimmedRequestedNodeId);
|
|
5033
|
+
// Only the applicant or a stage the case can already have passed: every
|
|
5034
|
+
// other node would skip ahead, re-run a gateway, or — the end event —
|
|
5035
|
+
// approve the case outright.
|
|
5036
|
+
if (!(0, workflow_graph_1.readSelectableReturnTargetNodeIds)(workflow, currentNodeId).includes(trimmedRequestedNodeId)) {
|
|
5037
|
+
throw new common_1.ConflictException(`User task ${currentNodeId} can only return to the applicant or an earlier user task, not ${trimmedRequestedNodeId}`);
|
|
5038
|
+
}
|
|
4900
5039
|
return trimmedRequestedNodeId;
|
|
4901
5040
|
}
|
|
4902
5041
|
if (trimmedRequestedNodeId &&
|
|
@@ -5081,6 +5220,10 @@ function capDecisionPolicyToCandidateCount(policy, candidateCount) {
|
|
|
5081
5220
|
? { ...policy, threshold: Math.max(candidateCount, 1) }
|
|
5082
5221
|
: policy;
|
|
5083
5222
|
}
|
|
5223
|
+
/** Splits `items` into consecutive runs of at most `size`. */
|
|
5224
|
+
function chunkArray(items, size) {
|
|
5225
|
+
return Array.from({ length: Math.ceil(items.length / size) }, (_, index) => items.slice(index * size, (index + 1) * size));
|
|
5226
|
+
}
|
|
5084
5227
|
function uniqueTasksById(tasks) {
|
|
5085
5228
|
const seen = new Set();
|
|
5086
5229
|
return tasks.filter((task) => {
|
|
@@ -5545,6 +5688,7 @@ function readCandidateStatusForClosedTask(taskStatus) {
|
|
|
5545
5688
|
*/
|
|
5546
5689
|
function validateSubmittedFormData(schema, formData, options = {}) {
|
|
5547
5690
|
const deferredFieldKeys = options.deferredFieldKeys ?? new Set();
|
|
5691
|
+
const testPattern = (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)();
|
|
5548
5692
|
const visibleFields = schema.fields.filter((field) => isSubmittedFormFieldVisible(field, schema.fields, formData));
|
|
5549
5693
|
// Tables answer "is this filled?" by row count and report per-row problems
|
|
5550
5694
|
// with an instance path, so they get their own pass instead of the flat
|
|
@@ -5555,6 +5699,25 @@ function validateSubmittedFormData(schema, formData, options = {}) {
|
|
|
5555
5699
|
!isSubmittedFieldValuePresent(formData[field.fieldKey]));
|
|
5556
5700
|
throwFirstFormDataFieldError({
|
|
5557
5701
|
missingFields,
|
|
5702
|
+
// A field an approver fills in later is held to its rules when they fill
|
|
5703
|
+
// it, not here: the applicant cannot see or change it, so only its type is
|
|
5704
|
+
// checked, the same as a table nobody fills in.
|
|
5705
|
+
readTextErrors: () => [
|
|
5706
|
+
...visibleFields.flatMap((field) => readSubmittedTextValueErrors(field, formData[field.fieldKey], {
|
|
5707
|
+
shapeOnly: deferredFieldKeys.has(field.fieldKey),
|
|
5708
|
+
testPattern,
|
|
5709
|
+
})),
|
|
5710
|
+
// A field nobody can fill in right now — hidden (store-only), or one
|
|
5711
|
+
// whose `visibleWhen` is false — keeps its submitted value, so its value
|
|
5712
|
+
// is checked for its type. Only the type: its length and format apply
|
|
5713
|
+
// once the field is shown and the filler can correct it.
|
|
5714
|
+
...schema.fields
|
|
5715
|
+
.filter((field) => !visibleFields.includes(field))
|
|
5716
|
+
.flatMap((field) => readSubmittedTextValueErrors(field, formData[field.fieldKey], {
|
|
5717
|
+
shapeOnly: true,
|
|
5718
|
+
testPattern,
|
|
5719
|
+
})),
|
|
5720
|
+
],
|
|
5558
5721
|
// A `DYNAMIC_FORM` resolver reads a member value whether or not its field
|
|
5559
5722
|
// is visible, so a hidden field still holding a value must be well-formed
|
|
5560
5723
|
// too. Whether a hidden field is *required* stays a visibility question
|
|
@@ -5564,14 +5727,14 @@ function validateSubmittedFormData(schema, formData, options = {}) {
|
|
|
5564
5727
|
readTableErrors: () => [
|
|
5565
5728
|
...visibleFields
|
|
5566
5729
|
.filter(form_1.isTableFieldDefinition)
|
|
5567
|
-
.flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], isSubmittedFormFieldRequired(field, schema.fields, formData), deferredFieldKeys.has(field.fieldKey))),
|
|
5730
|
+
.flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], isSubmittedFormFieldRequired(field, schema.fields, formData), testPattern, deferredFieldKeys.has(field.fieldKey))),
|
|
5568
5731
|
// A hidden table is not "visible", but its stored value still has to be
|
|
5569
5732
|
// a well-formed table under the row cap: only the required / minimum
|
|
5570
5733
|
// checks are skipped, since nobody can fill the table in.
|
|
5571
5734
|
...schema.fields
|
|
5572
5735
|
.filter(form_1.isTableFieldDefinition)
|
|
5573
5736
|
.filter((field) => field.hidden === true)
|
|
5574
|
-
.flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], false, true)),
|
|
5737
|
+
.flatMap((field) => validateSubmittedTableValue(field, formData[field.fieldKey], false, testPattern, true)),
|
|
5575
5738
|
],
|
|
5576
5739
|
});
|
|
5577
5740
|
}
|
|
@@ -5584,6 +5747,7 @@ function validateSubmittedFormData(schema, formData, options = {}) {
|
|
|
5584
5747
|
*/
|
|
5585
5748
|
function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys) {
|
|
5586
5749
|
const fields = schema.fields.filter((field) => fieldKeys.has(field.fieldKey));
|
|
5750
|
+
const testPattern = (0, text_field_pattern_test_1.createTimedTextFieldPatternTest)();
|
|
5587
5751
|
// `readRowCellValue` is used below as an own-property reader for top-level
|
|
5588
5752
|
// form keys: a key like `constructor` must not read off the prototype chain.
|
|
5589
5753
|
throwFirstFormDataFieldError({
|
|
@@ -5591,23 +5755,29 @@ function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys
|
|
|
5591
5755
|
missingFields: fields.filter((field) => !(0, form_1.isTableFieldDefinition)(field) &&
|
|
5592
5756
|
requiredFieldKeys.has(field.fieldKey) &&
|
|
5593
5757
|
(0, bpm_core_shared_1.isFormDataValueEmpty)(readRowCellValue(formData, field.fieldKey))),
|
|
5758
|
+
readTextErrors: () => fields.flatMap((field) => readSubmittedTextValueErrors(field, readRowCellValue(formData, field.fieldKey), { testPattern })),
|
|
5594
5759
|
readMemberErrors: () => (0, form_member_value_validator_1.readSubmittedMemberValueErrors)(fields, formData),
|
|
5595
5760
|
readTableErrors: () => fields
|
|
5596
5761
|
.filter(form_1.isTableFieldDefinition)
|
|
5597
|
-
.flatMap((field) => validateSubmittedTableValue(field, readRowCellValue(formData, field.fieldKey), requiredFieldKeys.has(field.fieldKey))),
|
|
5762
|
+
.flatMap((field) => validateSubmittedTableValue(field, readRowCellValue(formData, field.fieldKey), requiredFieldKeys.has(field.fieldKey), testPattern)),
|
|
5598
5763
|
});
|
|
5599
5764
|
}
|
|
5600
5765
|
/**
|
|
5601
5766
|
* The shared tail of submit and approver-fill validation: a missing required
|
|
5602
|
-
* field wins over a
|
|
5603
|
-
*
|
|
5767
|
+
* 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.
|
|
5604
5770
|
*/
|
|
5605
|
-
function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTableErrors, }) {
|
|
5771
|
+
function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTableErrors, readTextErrors, }) {
|
|
5606
5772
|
if (missingFields.length > 0) {
|
|
5607
5773
|
throw new common_1.BadRequestException(`Form data is missing required fields: ${missingFields
|
|
5608
5774
|
.map((field) => field.label || field.fieldKey)
|
|
5609
5775
|
.join(', ')}`);
|
|
5610
5776
|
}
|
|
5777
|
+
const textErrors = readTextErrors();
|
|
5778
|
+
if (textErrors.length > 0) {
|
|
5779
|
+
throw new common_1.BadRequestException(textErrors.join('; '));
|
|
5780
|
+
}
|
|
5611
5781
|
const memberErrors = readMemberErrors();
|
|
5612
5782
|
if (memberErrors.length > 0) {
|
|
5613
5783
|
throw new common_1.BadRequestException(memberErrors.join('; '));
|
|
@@ -5619,11 +5789,10 @@ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readTab
|
|
|
5619
5789
|
}
|
|
5620
5790
|
/**
|
|
5621
5791
|
* Authoritative shape, row-count and per-row required checks for one table
|
|
5622
|
-
* value (ADR 16 §3.7)
|
|
5623
|
-
*
|
|
5624
|
-
* and friends stay a frontend hint.
|
|
5792
|
+
* value (ADR 16 §3.7), plus the length and format rules of text columns, which
|
|
5793
|
+
* the flat text fields share. `minimum` / `maximum` stay a frontend hint.
|
|
5625
5794
|
*/
|
|
5626
|
-
function validateSubmittedTableValue(field, value, required, shapeOnly = false) {
|
|
5795
|
+
function validateSubmittedTableValue(field, value, required, testPattern, shapeOnly = false) {
|
|
5627
5796
|
const path = `formData.${field.fieldKey}`;
|
|
5628
5797
|
if (typeof value === 'undefined' || value === null) {
|
|
5629
5798
|
return shapeOnly
|
|
@@ -5635,31 +5804,32 @@ function validateSubmittedTableValue(field, value, required, shapeOnly = false)
|
|
|
5635
5804
|
if (!Array.isArray(value)) {
|
|
5636
5805
|
return [`${path} must be an array of rows`];
|
|
5637
5806
|
}
|
|
5638
|
-
|
|
5639
|
-
//
|
|
5640
|
-
|
|
5807
|
+
// Too many rows is refused before any row is checked: the rows a submitter
|
|
5808
|
+
// sends would otherwise decide how many cells the text patterns run on.
|
|
5809
|
+
const maxRows = readTableMaxRows(field);
|
|
5810
|
+
if (value.length > maxRows) {
|
|
5811
|
+
return [`${path} must include at most ${maxRows} row(s)`];
|
|
5812
|
+
}
|
|
5813
|
+
const rowErrors = value.flatMap((row, index) => validateSubmittedTableRow(field, row, `${path}[${index}]`, testPattern, shapeOnly));
|
|
5814
|
+
// A malformed row cannot be counted against the minimum meaningfully; report
|
|
5815
|
+
// the shape problem first so the submitter fixes the real cause.
|
|
5641
5816
|
if (rowErrors.length > 0) {
|
|
5642
5817
|
return rowErrors;
|
|
5643
5818
|
}
|
|
5644
5819
|
return validateSubmittedTableRowCount(field, value.length, required, path, shapeOnly);
|
|
5645
5820
|
}
|
|
5821
|
+
/** The minimum only: the maximum is refused before any row is checked. */
|
|
5646
5822
|
function validateSubmittedTableRowCount(field, rowCount, required, path, shapeOnly = false) {
|
|
5647
5823
|
const minRows = shapeOnly
|
|
5648
5824
|
? 0
|
|
5649
5825
|
: required
|
|
5650
5826
|
? Math.max(readTableMinRows(field), 1)
|
|
5651
5827
|
: readTableMinRows(field);
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
...(rowCount > maxRows
|
|
5658
|
-
? [`${path} must include at most ${maxRows} row(s)`]
|
|
5659
|
-
: []),
|
|
5660
|
-
];
|
|
5661
|
-
}
|
|
5662
|
-
function validateSubmittedTableRow(field, row, path, shapeOnly = false) {
|
|
5828
|
+
return rowCount < minRows
|
|
5829
|
+
? [`${path} must include at least ${minRows} row(s)`]
|
|
5830
|
+
: [];
|
|
5831
|
+
}
|
|
5832
|
+
function validateSubmittedTableRow(field, row, path, testPattern, shapeOnly = false) {
|
|
5663
5833
|
if (!isRecord(row)) {
|
|
5664
5834
|
return [`${path} must be an object`];
|
|
5665
5835
|
}
|
|
@@ -5676,7 +5846,61 @@ function validateSubmittedTableRow(field, row, path, shapeOnly = false) {
|
|
|
5676
5846
|
.filter((column) => isSubmittedTableColumnRequired(column) &&
|
|
5677
5847
|
!isSubmittedFieldValuePresent(readRowCellValue(row, column.fieldKey)))
|
|
5678
5848
|
.map((column) => `${path}.${column.fieldKey} is required`);
|
|
5679
|
-
|
|
5849
|
+
// A shape-only pass covers a table nobody fills in, so the filler-facing
|
|
5850
|
+
// rules do not apply to it, the same as the required check above; only a
|
|
5851
|
+
// cell of the wrong type is still refused.
|
|
5852
|
+
const textErrors = field.columns.flatMap((column) => readSubmittedTextValueErrors(column, readRowCellValue(row, column.fieldKey), {
|
|
5853
|
+
path: `${path}.${column.fieldKey}`,
|
|
5854
|
+
shapeOnly,
|
|
5855
|
+
testPattern,
|
|
5856
|
+
}));
|
|
5857
|
+
return [...unknownKeyErrors, ...cellErrors, ...requiredErrors, ...textErrors];
|
|
5858
|
+
}
|
|
5859
|
+
/**
|
|
5860
|
+
* The server half of the text length and format rules: the same
|
|
5861
|
+
* `readTextFieldValueIssue` the renderer runs before it lets the filler send,
|
|
5862
|
+
* so a crafted request cannot store what the form would have refused — a
|
|
5863
|
+
* number or an array sent for a text field with rules included. `shapeOnly`
|
|
5864
|
+
* keeps only that type check, for a value nobody fills in at this step, and
|
|
5865
|
+
* does not run the pattern at all. `testPattern` is the validation pass's
|
|
5866
|
+
* timed pattern test (see `createTimedTextFieldPatternTest`); once its time
|
|
5867
|
+
* budget is spent, a value is reported as not checked rather than as a
|
|
5868
|
+
* format error.
|
|
5869
|
+
*/
|
|
5870
|
+
function readSubmittedTextValueErrors(field, value, { path = `formData.${field.fieldKey}`, shapeOnly = false, testPattern, }) {
|
|
5871
|
+
if (field.type !== 'text' && field.type !== 'textarea') {
|
|
5872
|
+
return [];
|
|
5873
|
+
}
|
|
5874
|
+
const readIssue = () => {
|
|
5875
|
+
try {
|
|
5876
|
+
return (0, form_1.readTextFieldValueIssue)(field, value, {
|
|
5877
|
+
testPattern: shapeOnly ? () => true : testPattern,
|
|
5878
|
+
});
|
|
5879
|
+
}
|
|
5880
|
+
catch (error) {
|
|
5881
|
+
if (error instanceof text_field_pattern_test_1.TextFieldPatternTimeoutError) {
|
|
5882
|
+
return 'timeout';
|
|
5883
|
+
}
|
|
5884
|
+
throw error;
|
|
5885
|
+
}
|
|
5886
|
+
};
|
|
5887
|
+
const issue = readIssue();
|
|
5888
|
+
if (issue === 'timeout') {
|
|
5889
|
+
return [`${path} could not be checked against the required format in time`];
|
|
5890
|
+
}
|
|
5891
|
+
if (!issue || (shapeOnly && issue.kind !== 'type')) {
|
|
5892
|
+
return [];
|
|
5893
|
+
}
|
|
5894
|
+
if (issue.kind === 'type') {
|
|
5895
|
+
return [`${path} must be text`];
|
|
5896
|
+
}
|
|
5897
|
+
if (issue.kind === 'minLength') {
|
|
5898
|
+
return [`${path} must be at least ${issue.limit} characters`];
|
|
5899
|
+
}
|
|
5900
|
+
if (issue.kind === 'maxLength') {
|
|
5901
|
+
return [`${path} must be at most ${issue.limit} characters`];
|
|
5902
|
+
}
|
|
5903
|
+
return [`${path} does not match the required format`];
|
|
5680
5904
|
}
|
|
5681
5905
|
/**
|
|
5682
5906
|
* Own-property read. `constructor`, `toString` and friends all satisfy the
|