@rytass/bpm-core-nestjs-module 0.22.0 → 0.23.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 +64 -0
- package/package.json +2 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/bpm/bpm-root-options.d.ts +57 -0
- package/src/lib/bpm/bpm-root-options.js.map +1 -1
- package/src/lib/bpm/bpm-root.module.d.ts +21 -0
- package/src/lib/bpm/bpm-root.module.js +6 -0
- package/src/lib/bpm/bpm-root.module.js.map +1 -1
- package/src/lib/form/dto/form-definition.input.d.ts +5 -0
- package/src/lib/form/dto/form-definition.input.js.map +1 -1
- package/src/lib/form/form-definition.entity.d.ts +5 -0
- package/src/lib/form/form-definition.entity.js +7 -0
- package/src/lib/form/form-definition.entity.js.map +1 -1
- package/src/lib/form/form-schema.validator.js +17 -5
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/form/form.queries.d.ts +2 -2
- package/src/lib/form/form.queries.js +17 -5
- package/src/lib/form/form.queries.js.map +1 -1
- package/src/lib/form/form.service.d.ts +2 -1
- package/src/lib/form/form.service.js +10 -5
- package/src/lib/form/form.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js +22 -2
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.errors.d.ts +11 -0
- package/src/lib/form-data-source/form-data-source.errors.js +14 -1
- package/src/lib/form-data-source/form-data-source.errors.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.service.d.ts +6 -0
- package/src/lib/form-data-source/form-data-source.service.js +37 -3
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/instance-event/approval-instance-event.entity.d.ts +27 -0
- package/src/lib/instance-event/approval-instance-event.entity.js +93 -0
- package/src/lib/instance-event/approval-instance-event.entity.js.map +1 -0
- package/src/lib/instance-event/index.d.ts +13 -0
- package/src/lib/instance-event/index.js +17 -0
- package/src/lib/instance-event/index.js.map +1 -0
- package/src/lib/instance-event/instance-event.delivery-scheduler.service.d.ts +21 -0
- package/src/lib/instance-event/instance-event.delivery-scheduler.service.js +81 -0
- package/src/lib/instance-event/instance-event.delivery-scheduler.service.js.map +1 -0
- package/src/lib/instance-event/instance-event.delivery.service.d.ts +69 -0
- package/src/lib/instance-event/instance-event.delivery.service.js +259 -0
- package/src/lib/instance-event/instance-event.delivery.service.js.map +1 -0
- package/src/lib/instance-event/instance-event.delivery.subscriber.d.ts +24 -0
- package/src/lib/instance-event/instance-event.delivery.subscriber.js +85 -0
- package/src/lib/instance-event/instance-event.delivery.subscriber.js.map +1 -0
- package/src/lib/instance-event/instance-event.enums.d.ts +12 -0
- package/src/lib/instance-event/instance-event.enums.js +28 -0
- package/src/lib/instance-event/instance-event.enums.js.map +1 -0
- package/src/lib/instance-event/instance-event.module.d.ts +13 -0
- package/src/lib/instance-event/instance-event.module.js +52 -0
- package/src/lib/instance-event/instance-event.module.js.map +1 -0
- package/src/lib/instance-event/instance-event.object.d.ts +24 -0
- package/src/lib/instance-event/instance-event.object.js +73 -0
- package/src/lib/instance-event/instance-event.object.js.map +1 -0
- package/src/lib/instance-event/instance-event.options.d.ts +26 -0
- package/src/lib/instance-event/instance-event.options.js +35 -0
- package/src/lib/instance-event/instance-event.options.js.map +1 -0
- package/src/lib/instance-event/instance-event.resolver.d.ts +10 -0
- package/src/lib/instance-event/instance-event.resolver.js +75 -0
- package/src/lib/instance-event/instance-event.resolver.js.map +1 -0
- package/src/lib/instance-event/instance-event.service.d.ts +18 -0
- package/src/lib/instance-event/instance-event.service.js +37 -0
- package/src/lib/instance-event/instance-event.service.js.map +1 -0
- package/src/lib/instance-event/instance-event.token.d.ts +19 -0
- package/src/lib/instance-event/instance-event.token.js +9 -0
- package/src/lib/instance-event/instance-event.token.js.map +1 -0
- package/src/lib/instance-event/instance-event.types.d.ts +67 -0
- package/src/lib/instance-event/instance-event.types.js +9 -0
- package/src/lib/instance-event/instance-event.types.js.map +1 -0
- package/src/lib/instance-event/instance-event.writer.d.ts +21 -0
- package/src/lib/instance-event/instance-event.writer.js +45 -0
- package/src/lib/instance-event/instance-event.writer.js.map +1 -0
- package/src/lib/migrations/0000000032000-host-integration-round.d.ts +25 -0
- package/src/lib/migrations/0000000032000-host-integration-round.js +104 -0
- package/src/lib/migrations/0000000032000-host-integration-round.js.map +1 -0
- package/src/lib/migrations/index.d.ts +1 -0
- package/src/lib/migrations/index.js +3 -0
- package/src/lib/migrations/index.js.map +1 -1
- package/src/lib/notification/notification.service.js +50 -9
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/template/approval-template-version.entity.d.ts +9 -1
- package/src/lib/template/approval-template-version.entity.js +11 -1
- package/src/lib/template/approval-template-version.entity.js.map +1 -1
- package/src/lib/template/approval-template.entity.d.ts +18 -0
- package/src/lib/template/approval-template.entity.js +23 -0
- package/src/lib/template/approval-template.entity.js.map +1 -1
- package/src/lib/template/dto/approval-template.input.d.ts +21 -0
- package/src/lib/template/dto/approval-template.input.js +38 -0
- package/src/lib/template/dto/approval-template.input.js.map +1 -1
- package/src/lib/template/dto/compose-approval-template.input.d.ts +23 -0
- package/src/lib/template/dto/compose-approval-template.input.js +37 -0
- package/src/lib/template/dto/compose-approval-template.input.js.map +1 -1
- package/src/lib/template/template-key.d.ts +26 -0
- package/src/lib/template/template-key.js +67 -0
- package/src/lib/template/template-key.js.map +1 -0
- package/src/lib/template/template.enums.d.ts +11 -0
- package/src/lib/template/template.enums.js +17 -1
- package/src/lib/template/template.enums.js.map +1 -1
- package/src/lib/template/template.queries.d.ts +3 -2
- package/src/lib/template/template.queries.js +29 -4
- package/src/lib/template/template.queries.js.map +1 -1
- package/src/lib/template/template.service.d.ts +8 -1
- package/src/lib/template/template.service.js +94 -17
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/workflow-engine/approver-filled-table-columns.d.ts +91 -0
- package/src/lib/workflow-engine/approver-filled-table-columns.js +301 -0
- package/src/lib/workflow-engine/approver-filled-table-columns.js.map +1 -0
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.d.ts +7 -0
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js +9 -0
- package/src/lib/workflow-engine/dto/dry-run-approval-workflow.input.js.map +1 -1
- package/src/lib/workflow-engine/index.d.ts +1 -0
- package/src/lib/workflow-engine/index.js +1 -0
- package/src/lib/workflow-engine/index.js.map +1 -1
- package/src/lib/workflow-engine/instance-guard.runner.d.ts +19 -0
- package/src/lib/workflow-engine/instance-guard.runner.js +102 -0
- package/src/lib/workflow-engine/instance-guard.runner.js.map +1 -0
- package/src/lib/workflow-engine/instance-guard.token.d.ts +101 -0
- package/src/lib/workflow-engine/instance-guard.token.js +9 -0
- package/src/lib/workflow-engine/instance-guard.token.js.map +1 -0
- package/src/lib/workflow-engine/missing-form-fields.d.ts +12 -0
- package/src/lib/workflow-engine/missing-form-fields.js +27 -0
- package/src/lib/workflow-engine/missing-form-fields.js.map +1 -0
- package/src/lib/workflow-engine/workflow-engine.module.d.ts +2 -0
- package/src/lib/workflow-engine/workflow-engine.module.js +3 -0
- package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.d.ts +1 -1
- package/src/lib/workflow-engine/workflow-engine.mutations.js +8 -3
- package/src/lib/workflow-engine/workflow-engine.mutations.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.queries.d.ts +7 -7
- package/src/lib/workflow-engine/workflow-engine.queries.js +97 -28
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +141 -7
- package/src/lib/workflow-engine/workflow-engine.service.js +653 -75
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.tokens.d.ts +5 -0
|
@@ -36,7 +36,9 @@ const organization_enums_1 = require("../organization/organization.enums");
|
|
|
36
36
|
const organization_time_zone_1 = require("../organization/organization-time-zone");
|
|
37
37
|
const approval_template_version_entity_1 = require("../template/approval-template-version.entity");
|
|
38
38
|
const approval_template_entity_1 = require("../template/approval-template.entity");
|
|
39
|
+
const approver_filled_table_columns_1 = require("./approver-filled-table-columns");
|
|
39
40
|
const template_enums_1 = require("../template/template.enums");
|
|
41
|
+
const missing_form_fields_1 = require("./missing-form-fields");
|
|
40
42
|
const activity_log_entity_1 = require("./activity-log.entity");
|
|
41
43
|
const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
|
|
42
44
|
const adhoc_enums_1 = require("./adhoc.enums");
|
|
@@ -58,6 +60,9 @@ const expression_budget_1 = require("../condition/expression-budget");
|
|
|
58
60
|
const expression_errors_1 = require("../condition/expression-errors");
|
|
59
61
|
const expression_dialect_1 = require("../condition/expression-dialect");
|
|
60
62
|
const initiator_custom_fields_resolver_token_1 = require("./initiator-custom-fields-resolver.token");
|
|
63
|
+
const instance_event_writer_1 = require("../instance-event/instance-event.writer");
|
|
64
|
+
const instance_guard_token_1 = require("./instance-guard.token");
|
|
65
|
+
const instance_guard_runner_1 = require("./instance-guard.runner");
|
|
61
66
|
const workflow_service_task_dispatcher_token_1 = require("./workflow-service-task-dispatcher.token");
|
|
62
67
|
const workflow_token_entity_1 = require("./workflow-token.entity");
|
|
63
68
|
const bpm_root_options_1 = require("../bpm/bpm-root-options");
|
|
@@ -182,6 +187,28 @@ const INACTIVE_MEMBER_NAME_LOCALE = 'zh-Hant-TW';
|
|
|
182
187
|
* every load of the member screen.
|
|
183
188
|
*/
|
|
184
189
|
const INACTIVE_MEMBER_COUNT_CONCURRENCY = 5;
|
|
190
|
+
/**
|
|
191
|
+
* ADR 27 §4.2: an approver-filled value the provider no longer offers answers
|
|
192
|
+
* 400 with `formDataErrors` naming the field or table cell, `code` kept. Other
|
|
193
|
+
* failures pass through unchanged.
|
|
194
|
+
*/
|
|
195
|
+
async function answerValueNotResolved(run) {
|
|
196
|
+
try {
|
|
197
|
+
return await run();
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error instanceof form_data_source_errors_1.BPMFormDataSourceValueNotResolvedException) {
|
|
201
|
+
const issue = {
|
|
202
|
+
...(0, form_data_issue_1.createFormDataIssue)(error.location, 'VALUE_NOT_RESOLVED', '的選項已不在可選範圍內,請重新選擇'),
|
|
203
|
+
code: form_data_source_errors_1.BPM_FORM_DATA_SOURCE_ERROR_CODES.VALUE_NOT_RESOLVED,
|
|
204
|
+
};
|
|
205
|
+
throw (0, form_data_issue_1.createFormDataBadRequest)((0, form_data_issue_1.readFormDataIssueMessage)([issue]), [
|
|
206
|
+
issue,
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
185
212
|
const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
|
|
186
213
|
'bpm:*',
|
|
187
214
|
'bpm.workflow.read_all',
|
|
@@ -189,7 +216,7 @@ const WORKFLOW_READ_ALL_PERMISSIONS = new Set([
|
|
|
189
216
|
'workflow.read_all',
|
|
190
217
|
]);
|
|
191
218
|
let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineService {
|
|
192
|
-
constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, injectedServiceTaskDispatcher, formDataSourceValueResolver, rootOptions, webhookDeliveryService, injectedInitiatorCustomFieldsResolver) {
|
|
219
|
+
constructor(approvalInstanceRepository, workflowTokenRepository, taskRepository, taskCandidateRepository, taskDecisionRepository, notificationRepository, activityLogRepository, adhocDirectiveRepository, approvalTemplateRepository, approvalTemplateVersionRepository, formDefinitionVersionRepository, attachmentService, conditionService, delegationService, notificationService, signatureService, slaScheduleService, injectedServiceTaskDispatcher, formDataSourceValueResolver, rootOptions, webhookDeliveryService, injectedInitiatorCustomFieldsResolver, injectedInstanceGuard, instanceEventWriter) {
|
|
193
220
|
this.approvalInstanceRepository = approvalInstanceRepository;
|
|
194
221
|
this.workflowTokenRepository = workflowTokenRepository;
|
|
195
222
|
this.taskRepository = taskRepository;
|
|
@@ -209,6 +236,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
209
236
|
this.slaScheduleService = slaScheduleService;
|
|
210
237
|
this.formDataSourceValueResolver = formDataSourceValueResolver;
|
|
211
238
|
this.webhookDeliveryService = webhookDeliveryService;
|
|
239
|
+
this.instanceEventWriter = instanceEventWriter;
|
|
212
240
|
this.logger = new common_1.Logger(WorkflowEngineService_1.name);
|
|
213
241
|
/** In-flight inbox reads by member; see `readSharedOpenInboxTasks`. */
|
|
214
242
|
this.openInboxReadsInFlight = new Map();
|
|
@@ -236,7 +264,68 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
236
264
|
injectedInitiatorCustomFieldsResolver ??
|
|
237
265
|
rootOptions?.initiatorCustomFieldsResolver ??
|
|
238
266
|
null;
|
|
267
|
+
// Same three-way order as the initiator resolver: a host binding under the
|
|
268
|
+
// token, then the runtime instance, then no guard at all.
|
|
269
|
+
this.instanceGuard =
|
|
270
|
+
injectedInstanceGuard ?? rootOptions?.instanceGuard ?? null;
|
|
271
|
+
this.instanceGuardTimeoutMs =
|
|
272
|
+
rootOptions?.instanceGuardTimeoutMs ??
|
|
273
|
+
instance_guard_runner_1.BPM_INSTANCE_GUARD_DEFAULT_TIMEOUT_MS;
|
|
239
274
|
this.organizationTimeZone = (0, organization_time_zone_1.resolveBPMOrganizationTimeZone)(rootOptions?.organizationTimeZone);
|
|
275
|
+
this.expressionMissingFormFieldsAsNull =
|
|
276
|
+
rootOptions?.expressionMissingFormFieldsAsNull === true;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Runs the bound guard for one engine path inside its transaction. A no-op
|
|
280
|
+
* when no guard is bound, so a host without one sees no behavior change.
|
|
281
|
+
*/
|
|
282
|
+
async guardInstanceChange(input) {
|
|
283
|
+
if (this.instanceGuard === null) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
await (0, instance_guard_runner_1.runInstanceGuard)(this.instanceGuard, input, this.instanceGuardTimeoutMs);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Writes one case-lifecycle event (ADR 27 §1) in the caller's transaction.
|
|
290
|
+
* Returns before any lookup when no observer is bound, so a host without
|
|
291
|
+
* one pays nothing. `instanceState` is the state the change commits, not the
|
|
292
|
+
* state the instance held before it.
|
|
293
|
+
*/
|
|
294
|
+
async recordInstanceEvent(manager, instance, instanceState, type, fields) {
|
|
295
|
+
if (this.instanceEventWriter?.enabled !== true) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const template = await manager
|
|
299
|
+
.getRepository(approval_template_entity_1.ApprovalTemplateEntity)
|
|
300
|
+
.findOne({
|
|
301
|
+
select: { id: true, key: true },
|
|
302
|
+
where: { id: instance.templateId },
|
|
303
|
+
withDeleted: true,
|
|
304
|
+
});
|
|
305
|
+
const snapshot = {
|
|
306
|
+
instanceState,
|
|
307
|
+
templateId: instance.templateId,
|
|
308
|
+
templateKey: template?.key ?? null,
|
|
309
|
+
templateVersionId: instance.templateVersionId,
|
|
310
|
+
};
|
|
311
|
+
await this.instanceEventWriter.record(manager, instance.id, snapshot, type, fields);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Reads the template version a case runs on, inside the engine transaction,
|
|
315
|
+
* so the guard sees the same snapshot as the decision. A missing row fails
|
|
316
|
+
* the change rather than handing the guard a partial input.
|
|
317
|
+
*/
|
|
318
|
+
async readBoundTemplateVersionOrThrow(manager, instance) {
|
|
319
|
+
const version = await manager
|
|
320
|
+
.getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity)
|
|
321
|
+
.findOne({ where: { id: instance.templateVersionId } });
|
|
322
|
+
if (!version) {
|
|
323
|
+
throw new common_1.ConflictException({
|
|
324
|
+
code: 'TEMPLATE_VERSION_NOT_FOUND',
|
|
325
|
+
message: 'The template version of this instance no longer exists.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return version;
|
|
240
329
|
}
|
|
241
330
|
/**
|
|
242
331
|
* Today's date as the organization sees it (`organizationTimeZone`), for
|
|
@@ -308,6 +397,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
308
397
|
input.submittedByMemberId.trim() === '')) {
|
|
309
398
|
throw new common_1.BadRequestException('submittedByMemberId must be a non-empty string');
|
|
310
399
|
}
|
|
400
|
+
const hostCustomFields = readHostInitiatorCustomFields(input);
|
|
311
401
|
const submittedFormData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
312
402
|
const businessKey = normalizeBusinessKey(input.businessKey);
|
|
313
403
|
const submittedByMemberId = input.submittedByMemberId ?? input.initiatorMemberId;
|
|
@@ -319,6 +409,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
319
409
|
throw new common_1.ConflictException('Approval template does not have a published version');
|
|
320
410
|
}
|
|
321
411
|
const templateVersion = await this.getPublishedTemplateVersionOrThrow(template.currentVersionId);
|
|
412
|
+
if (input.endUserSubmission === true) {
|
|
413
|
+
assertEndUserMayLaunch(templateVersion);
|
|
414
|
+
}
|
|
322
415
|
const formDefinitionVersion = await this.getPublishedFormVersionOrThrow(templateVersion.formDefinitionVersionId);
|
|
323
416
|
// The fields an approver fills in belong to the approval, not the
|
|
324
417
|
// application: an end user's value for one is dropped (it would otherwise
|
|
@@ -327,13 +420,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
327
420
|
// checked here; required and row minimums wait for the node that fills
|
|
328
421
|
// them. Read off the version the instance is bound to, not the template's
|
|
329
422
|
// latest, so a publish in between cannot change what is stripped.
|
|
330
|
-
const
|
|
423
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(templateVersion.workflowDefinition);
|
|
331
424
|
const formData = input.endUserSubmission === true
|
|
332
|
-
? (0, bpm_core_shared_1.omitFormDataKeys)(submittedFormData,
|
|
425
|
+
? (0, approver_filled_table_columns_1.applyOpenColumnCells)((0, bpm_core_shared_1.omitFormDataKeys)(submittedFormData, deferral.wholeKeys), null, deferral)
|
|
333
426
|
: submittedFormData;
|
|
334
|
-
validateSubmittedFormData(formDefinitionVersion.schema, formData, {
|
|
335
|
-
deferredFieldKeys,
|
|
336
|
-
});
|
|
427
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(formDefinitionVersion.schema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
337
428
|
// Checked against the bound version's schema before any data source is
|
|
338
429
|
// called, so a bad value is refused cheaply. The transaction below
|
|
339
430
|
// confirms under the template lock that this version is still current
|
|
@@ -347,7 +438,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
347
438
|
});
|
|
348
439
|
const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
|
|
349
440
|
? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
|
|
350
|
-
: await this.buildInitiatorMetadataSnapshot({
|
|
441
|
+
: mergeHostInitiatorCustomFields(await this.buildInitiatorMetadataSnapshot({
|
|
351
442
|
authContext: submitAuthContext,
|
|
352
443
|
memberId: input.initiatorMemberId,
|
|
353
444
|
submission: {
|
|
@@ -355,7 +446,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
355
446
|
templateId: template.id,
|
|
356
447
|
templateVersionId: templateVersion.id,
|
|
357
448
|
},
|
|
358
|
-
});
|
|
449
|
+
}), hostCustomFields);
|
|
359
450
|
if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel',
|
|
360
451
|
// The policy is written in its version's dialect (ADR 22 §3.3).
|
|
361
452
|
readExpressionDialectOptions(templateVersion.initiatorPolicyCel, templateVersion.workflowDefinition))) {
|
|
@@ -396,7 +487,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
396
487
|
const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
|
|
397
488
|
const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
|
|
398
489
|
const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
|
|
399
|
-
const
|
|
490
|
+
const instanceDraft = instanceRepository.create({
|
|
400
491
|
completedAt: null,
|
|
401
492
|
formData,
|
|
402
493
|
formDataOptionSnapshot,
|
|
@@ -416,7 +507,24 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
416
507
|
templateVersionId: templateVersion.id,
|
|
417
508
|
title: input.title?.trim() || template.name,
|
|
418
509
|
workflowSnapshot: templateVersion.workflowDefinition,
|
|
419
|
-
})
|
|
510
|
+
});
|
|
511
|
+
// ADR 27 §2: the host guard sees the case as it is about to be saved,
|
|
512
|
+
// under the template lock. A refusal throws here, before any insert.
|
|
513
|
+
await this.guardInstanceChange({
|
|
514
|
+
action: null,
|
|
515
|
+
actorMemberId: submittedByMemberId,
|
|
516
|
+
authContext: authContext ?? null,
|
|
517
|
+
automatic: null,
|
|
518
|
+
changedFieldKeys: Object.keys(formData),
|
|
519
|
+
formData,
|
|
520
|
+
instance: instanceDraft,
|
|
521
|
+
kind: 'SUBMIT',
|
|
522
|
+
manager,
|
|
523
|
+
node: null,
|
|
524
|
+
task: null,
|
|
525
|
+
templateVersion,
|
|
526
|
+
});
|
|
527
|
+
const instance = await instanceRepository.save(instanceDraft);
|
|
420
528
|
await this.attachmentService.bindFormDataAttachmentsToInstance(manager, {
|
|
421
529
|
formData,
|
|
422
530
|
instanceId: instance.id,
|
|
@@ -454,6 +562,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
454
562
|
taskId: null,
|
|
455
563
|
}),
|
|
456
564
|
]);
|
|
565
|
+
await this.recordInstanceEvent(manager, instance, instance.state, 'INSTANCE_SUBMITTED', { actorMemberId: submittedByMemberId });
|
|
457
566
|
await this.processRunningInstance(manager, instance);
|
|
458
567
|
return instance;
|
|
459
568
|
}).catch((error) =>
|
|
@@ -494,6 +603,66 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
494
603
|
schema,
|
|
495
604
|
});
|
|
496
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* ADR 27 §4(b): asks the data-source providers again, under the decision
|
|
608
|
+
* lock and as the approver, for the options of the approver-fillable fields.
|
|
609
|
+
* A value that is no longer among the options is refused by the resolver
|
|
610
|
+
* (`VALUE_NOT_RESOLVED`). Returns snapshots for the scoped fields only; the
|
|
611
|
+
* caller swaps them in for the stored ones.
|
|
612
|
+
*/
|
|
613
|
+
async refreshApproverFilledOptionSnapshots({ approverAuthContext, fieldKeys, formData, instance, }) {
|
|
614
|
+
const schema = readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot);
|
|
615
|
+
return this.resolveFormDataOptionSnapshots({
|
|
616
|
+
authContext: approverAuthContext,
|
|
617
|
+
formData,
|
|
618
|
+
previousFormData: instance.formData,
|
|
619
|
+
previousSnapshots: readSnapshotWithoutFields(instance.formDataOptionSnapshot, fieldKeys),
|
|
620
|
+
revalidateAll: true,
|
|
621
|
+
schema: {
|
|
622
|
+
...schema,
|
|
623
|
+
fields: schema.fields.filter((field) => fieldKeys.has(field.fieldKey)),
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* ADR 27 §4(b): asks the data-source providers again, as the approver, for
|
|
629
|
+
* the open data-source cells of column-mode tables. `revalidateAll` false lets
|
|
630
|
+
* the resolver reuse the stored options of cells whose value and bindings did
|
|
631
|
+
* not move. Returns snapshots for the cells of `cellSchema` only.
|
|
632
|
+
*/
|
|
633
|
+
async refreshApproverFilledOpenCellSnapshots({ approverAuthContext, cellSchema, formData, instance, revalidateAll, }) {
|
|
634
|
+
return this.resolveFormDataOptionSnapshots({
|
|
635
|
+
authContext: approverAuthContext,
|
|
636
|
+
formData,
|
|
637
|
+
previousFormData: instance.formData,
|
|
638
|
+
previousSnapshots: instance.formDataOptionSnapshot ?? {},
|
|
639
|
+
revalidateAll,
|
|
640
|
+
schema: cellSchema,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Refuses an end user's resubmission of an instance bound to a `HOST_ONLY`
|
|
645
|
+
* version (ADR 27 §5). Checked before any data source is called and outside
|
|
646
|
+
* the lock: an instance never changes version and a published version never
|
|
647
|
+
* changes `launchMode`, so the answer cannot go stale. An instance that does
|
|
648
|
+
* not exist is left to the locked read, which reports it.
|
|
649
|
+
*/
|
|
650
|
+
async assertEndUserMayResubmit(instanceId) {
|
|
651
|
+
const instance = await this.approvalInstanceRepository.findOne({
|
|
652
|
+
select: { id: true, templateVersionId: true },
|
|
653
|
+
where: { id: instanceId },
|
|
654
|
+
});
|
|
655
|
+
if (!instance) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const version = await this.approvalTemplateVersionRepository.findOne({
|
|
659
|
+
select: { id: true, launchMode: true },
|
|
660
|
+
where: { id: instance.templateVersionId },
|
|
661
|
+
});
|
|
662
|
+
if (version) {
|
|
663
|
+
assertEndUserMayLaunch(version);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
497
666
|
async prepareResubmitFormDataOptions(input, submittedFormData, authContext) {
|
|
498
667
|
const instance = await this.approvalInstanceRepository.findOne({
|
|
499
668
|
where: { id: input.instanceId },
|
|
@@ -512,13 +681,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
512
681
|
throw new common_1.ConflictException('Approval template is deactivated');
|
|
513
682
|
}
|
|
514
683
|
const schema = readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot);
|
|
515
|
-
const
|
|
684
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(instance.workflowSnapshot);
|
|
516
685
|
// Same overlay as inside the lock, so the options resolved here are for
|
|
517
686
|
// the form data that is actually saved.
|
|
518
687
|
const formData = input.endUserSubmission === true
|
|
519
|
-
? (0, bpm_core_shared_1.overlayStoredFormDataKeys)(submittedFormData, instance.formData,
|
|
688
|
+
? (0, approver_filled_table_columns_1.applyOpenColumnCells)((0, bpm_core_shared_1.overlayStoredFormDataKeys)(submittedFormData, instance.formData, deferral.wholeKeys), instance.formData, deferral)
|
|
520
689
|
: submittedFormData;
|
|
521
|
-
validateSubmittedFormData(schema, formData, { deferredFieldKeys });
|
|
690
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(schema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
522
691
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData);
|
|
523
692
|
const formDataOptionSnapshot = await this.resolveFormDataOptionSnapshots({
|
|
524
693
|
authContext,
|
|
@@ -556,6 +725,26 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
556
725
|
await this.processRunningInstance(manager, instance);
|
|
557
726
|
});
|
|
558
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Records `TASK_AUTO_DECISION_FAILED` (ADR 27 §3) in its own transaction.
|
|
730
|
+
* Used after an automatic decision was refused by the instance guard: the
|
|
731
|
+
* decision's transaction has already rolled back, so the event cannot ride
|
|
732
|
+
* on it. Returns before any query when no observer is bound.
|
|
733
|
+
*/
|
|
734
|
+
async recordTaskAutoDecisionFailed(input) {
|
|
735
|
+
if (this.instanceEventWriter?.enabled !== true) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await this.runEngineTransaction(async (manager) => {
|
|
739
|
+
const instance = await manager
|
|
740
|
+
.getRepository(approval_instance_entity_1.ApprovalInstanceEntity)
|
|
741
|
+
.findOne({ where: { id: input.instanceId } });
|
|
742
|
+
if (!instance) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
await this.recordInstanceEvent(manager, instance, instance.state, 'TASK_AUTO_DECISION_FAILED', { reason: input.reason, taskId: input.taskId });
|
|
746
|
+
});
|
|
747
|
+
}
|
|
559
748
|
async decideTask(input, options = {}) {
|
|
560
749
|
return this.runEngineTransaction(async (manager) => {
|
|
561
750
|
const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
|
|
@@ -719,9 +908,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
719
908
|
if (filledFormData !== null && task.isAdhoc) {
|
|
720
909
|
throw new common_1.ForbiddenException(`Ad-hoc task ${task.id} cannot fill in form fields`);
|
|
721
910
|
}
|
|
911
|
+
// ADR 27 §4(a): column-mode tables are accepted here and settled
|
|
912
|
+
// cell by cell below. Their open data-source columns are re-resolved per
|
|
913
|
+
// row as the approver (ADR 27 §4(b)).
|
|
914
|
+
const nodeColumnFillMap = (0, approver_filled_table_columns_1.readNodeColumnFillMap)(taskNode);
|
|
722
915
|
if (filledFormData !== null) {
|
|
723
916
|
const fillableFieldKeys = (0, bpm_core_shared_1.readNodeFillableFieldKeys)(taskNode);
|
|
724
|
-
const refusedFieldKeys = Object.keys(filledFormData).filter((fieldKey) => !fillableFieldKeys.has(fieldKey)
|
|
917
|
+
const refusedFieldKeys = Object.keys(filledFormData).filter((fieldKey) => !fillableFieldKeys.has(fieldKey) &&
|
|
918
|
+
!nodeColumnFillMap.has(fieldKey));
|
|
725
919
|
if (refusedFieldKeys.length > 0) {
|
|
726
920
|
throw new common_1.ForbiddenException(`Workflow node ${task.nodeId} does not let approvers fill in: ${refusedFieldKeys.join(', ')}`);
|
|
727
921
|
}
|
|
@@ -732,35 +926,134 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
732
926
|
(0, form_data_fingerprint_1.computeFormDataFingerprint)(instance.formData)) {
|
|
733
927
|
throw new common_1.ConflictException(`Approval instance ${instance.id} form data changed; refresh and retry`);
|
|
734
928
|
}
|
|
929
|
+
// ADR 27 §4(a): each column-mode table is merged per cell against the
|
|
930
|
+
// stored rows. A changed row count, row order or applicant cell is
|
|
931
|
+
// refused before anything below sees the payload.
|
|
932
|
+
const settledFormData = filledFormData === null
|
|
933
|
+
? null
|
|
934
|
+
: (0, approver_filled_table_columns_1.settleColumnFillPayload)(filledFormData, instance.formData, nodeColumnFillMap);
|
|
735
935
|
// Only keys whose value really changes are written and reported. A key
|
|
736
936
|
// the form data does not have yet counts as changed even when the new
|
|
737
937
|
// value is null.
|
|
738
|
-
const changedFieldKeys =
|
|
938
|
+
const changedFieldKeys = settledFormData === null
|
|
739
939
|
? []
|
|
740
|
-
: Object.keys(
|
|
940
|
+
: Object.keys(settledFormData)
|
|
741
941
|
.filter((fieldKey) => !Object.prototype.hasOwnProperty.call(instance.formData, fieldKey) ||
|
|
742
|
-
(0, form_data_fingerprint_1.computeStableValueFingerprint)(
|
|
942
|
+
(0, form_data_fingerprint_1.computeStableValueFingerprint)(settledFormData[fieldKey]) !==
|
|
743
943
|
(0, form_data_fingerprint_1.computeStableValueFingerprint)(instance.formData[fieldKey]))
|
|
744
944
|
.sort();
|
|
745
|
-
const mergedFormData = { ...instance.formData, ...
|
|
945
|
+
const mergedFormData = { ...instance.formData, ...settledFormData };
|
|
946
|
+
// ADR 27 §4.3: an approver may change a column only if no column this
|
|
947
|
+
// approver may not fill reads it. Such a dependent keeps its stored value
|
|
948
|
+
// here, so a changed source would leave it stale. Fail closed.
|
|
949
|
+
if (nodeColumnFillMap.size > 0) {
|
|
950
|
+
(0, approver_filled_table_columns_1.assertDependentColumnsFrozen)(readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot), taskNode, instance.formData, mergedFormData);
|
|
951
|
+
}
|
|
746
952
|
// Every approval at a node that lets its approver fill in fields is
|
|
747
953
|
// held to those fields, whether or not this request sent any: the host
|
|
748
|
-
// may have pre-filled a table the approver then leaves as is.
|
|
954
|
+
// may have pre-filled a table the approver then leaves as is. A column
|
|
955
|
+
// this node grants REQUIRED is required in the check, not only in the
|
|
956
|
+
// template.
|
|
749
957
|
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED && !task.isAdhoc) {
|
|
750
|
-
const nodeFillableFieldKeys =
|
|
958
|
+
const nodeFillableFieldKeys = new Set([
|
|
959
|
+
...(0, bpm_core_shared_1.readNodeFillableFieldKeys)(taskNode),
|
|
960
|
+
...nodeColumnFillMap.keys(),
|
|
961
|
+
]);
|
|
751
962
|
if (nodeFillableFieldKeys.size > 0) {
|
|
752
|
-
validateFilledFormFields(readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot), mergedFormData, nodeFillableFieldKeys, (0, bpm_core_shared_1.readNodeRequiredFieldKeys)(taskNode));
|
|
963
|
+
validateFilledFormFields((0, approver_filled_table_columns_1.projectNodeOpenColumnsRequired)(readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot), nodeColumnFillMap), mergedFormData, nodeFillableFieldKeys, (0, bpm_core_shared_1.readNodeRequiredFieldKeys)(taskNode));
|
|
753
964
|
}
|
|
754
965
|
}
|
|
755
|
-
|
|
966
|
+
// ADR 27 §4(b): an approver-filled data-source field is judged as the
|
|
967
|
+
// approver's own pick. The provider is asked again under the lock with
|
|
968
|
+
// the approver's identity, so a value outside the current options is
|
|
969
|
+
// refused and the recorded label is refreshed before anything is
|
|
970
|
+
// written. Table data-source columns resolve per row (ROW_FIELD).
|
|
971
|
+
const fillableFieldKeys = (0, bpm_core_shared_1.readNodeFillableFieldKeys)(taskNode);
|
|
972
|
+
const dataSourceRefreshKeys = input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED && !task.isAdhoc
|
|
973
|
+
? fillableFieldKeys
|
|
974
|
+
: new Set(changedFieldKeys.filter((fieldKey) => fillableFieldKeys.has(fieldKey)));
|
|
975
|
+
const approverAuthContext = options.approverAuthContext ??
|
|
976
|
+
createWorkflowAuthContext(input.decidedByMemberId);
|
|
977
|
+
const refreshedFormDataOptionSnapshots = dataSourceRefreshKeys.size === 0
|
|
978
|
+
? null
|
|
979
|
+
: await answerValueNotResolved(() => this.refreshApproverFilledOptionSnapshots({
|
|
980
|
+
approverAuthContext,
|
|
981
|
+
fieldKeys: dataSourceRefreshKeys,
|
|
982
|
+
formData: mergedFormData,
|
|
983
|
+
instance,
|
|
984
|
+
}));
|
|
985
|
+
const wholeFieldOptionSnapshot = refreshedFormDataOptionSnapshots === null
|
|
986
|
+
? (instance.formDataOptionSnapshot ?? {})
|
|
987
|
+
: {
|
|
988
|
+
...readSnapshotWithoutFields(instance.formDataOptionSnapshot, dataSourceRefreshKeys),
|
|
989
|
+
...refreshedFormDataOptionSnapshots,
|
|
990
|
+
};
|
|
991
|
+
// ADR 27 §4(b): the open data-source cells of column-mode tables. An
|
|
992
|
+
// APPROVED decision re-resolves all of them; any other decision only
|
|
993
|
+
// when a column-mode table's cells changed. The form schema is read
|
|
994
|
+
// only for a node that has column-mode fills, so other nodes never
|
|
995
|
+
// touch the snapshot's schema.
|
|
996
|
+
const openCellSchema = nodeColumnFillMap.size === 0
|
|
997
|
+
? null
|
|
998
|
+
: (0, approver_filled_table_columns_1.readOpenDataSourceCellSchema)(readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot), nodeColumnFillMap);
|
|
999
|
+
const openCellTableKeys = new Set((openCellSchema?.fields ?? []).map((field) => field.fieldKey));
|
|
1000
|
+
const revalidateAllOpenCells = input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED && !task.isAdhoc;
|
|
1001
|
+
const hasChangedOpenCellTable = changedFieldKeys.some((fieldKey) => openCellTableKeys.has(fieldKey));
|
|
1002
|
+
const refreshedOpenCellSnapshots = openCellSchema === null ||
|
|
1003
|
+
openCellSchema.fields.length === 0 ||
|
|
1004
|
+
(!revalidateAllOpenCells && !hasChangedOpenCellTable)
|
|
1005
|
+
? null
|
|
1006
|
+
: await answerValueNotResolved(() => this.refreshApproverFilledOpenCellSnapshots({
|
|
1007
|
+
approverAuthContext,
|
|
1008
|
+
cellSchema: openCellSchema,
|
|
1009
|
+
formData: mergedFormData,
|
|
1010
|
+
instance,
|
|
1011
|
+
revalidateAll: revalidateAllOpenCells,
|
|
1012
|
+
}));
|
|
1013
|
+
const nextFormDataOptionSnapshot = refreshedOpenCellSnapshots === null || openCellSchema === null
|
|
1014
|
+
? wholeFieldOptionSnapshot
|
|
1015
|
+
: {
|
|
1016
|
+
...(0, approver_filled_table_columns_1.readSnapshotWithoutOpenCells)(wholeFieldOptionSnapshot, openCellSchema),
|
|
1017
|
+
...(0, approver_filled_table_columns_1.readOpenCellSnapshots)(refreshedOpenCellSnapshots, openCellSchema),
|
|
1018
|
+
};
|
|
1019
|
+
// ADR 27 §2: the host guard judges the decision as it will be
|
|
1020
|
+
// committed, after the form checks and before anything is written.
|
|
1021
|
+
// The version is read only when a guard is bound, so an engine
|
|
1022
|
+
// without one issues exactly the queries it did before.
|
|
1023
|
+
if (this.instanceGuard !== null) {
|
|
1024
|
+
await this.guardInstanceChange({
|
|
1025
|
+
action: input.action,
|
|
1026
|
+
actorMemberId: input.decidedByMemberId,
|
|
1027
|
+
// The approver's own context when the GraphQL mutation passed one;
|
|
1028
|
+
// never the member-id stub the data-source path falls back to.
|
|
1029
|
+
authContext: options.approverAuthContext ?? null,
|
|
1030
|
+
automatic: options.automatic ?? null,
|
|
1031
|
+
changedFieldKeys,
|
|
1032
|
+
formData: mergedFormData,
|
|
1033
|
+
instance,
|
|
1034
|
+
kind: 'DECIDE',
|
|
1035
|
+
manager,
|
|
1036
|
+
node: taskNode,
|
|
1037
|
+
task,
|
|
1038
|
+
templateVersion: await this.readBoundTemplateVersionOrThrow(manager, instance),
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
const formDataChanged = settledFormData !== null && changedFieldKeys.length > 0;
|
|
1042
|
+
const formDataOptionSnapshotChanged = (0, form_data_fingerprint_1.computeStableValueFingerprint)(nextFormDataOptionSnapshot) !==
|
|
1043
|
+
(0, form_data_fingerprint_1.computeStableValueFingerprint)(instance.formDataOptionSnapshot ?? {});
|
|
1044
|
+
if (formDataChanged) {
|
|
756
1045
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, mergedFormData, manager);
|
|
1046
|
+
}
|
|
1047
|
+
if (formDataChanged || formDataOptionSnapshotChanged) {
|
|
757
1048
|
// Merged into the shared `instance` before it is signed and before
|
|
758
1049
|
// the token moves on: the signature must cover the filled values, a
|
|
759
1050
|
// gateway after this node must route on them, and an end node saves
|
|
760
|
-
// this same object, which would write the old values back.
|
|
1051
|
+
// this same object, which would write the old values back. The
|
|
1052
|
+
// refreshed option labels go in the same write.
|
|
761
1053
|
const savedInstance = await instanceRepository.save({
|
|
762
1054
|
...instance,
|
|
763
1055
|
formData: mergedFormData,
|
|
1056
|
+
formDataOptionSnapshot: nextFormDataOptionSnapshot,
|
|
764
1057
|
});
|
|
765
1058
|
Object.assign(instance, savedInstance);
|
|
766
1059
|
}
|
|
@@ -879,7 +1172,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
879
1172
|
// (rejected / returned, or an approval that completed it), every other
|
|
880
1173
|
// candidate's notification is superseded too; a non-completing approval
|
|
881
1174
|
// on a multi-approver task leaves the remaining approvers' open.
|
|
882
|
-
const taskEnded = input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED ||
|
|
1175
|
+
const taskEnded = input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED ||
|
|
1176
|
+
shouldCompleteTask;
|
|
883
1177
|
await this.notificationService.resolveTaskNotifications({
|
|
884
1178
|
actingMemberId: input.decidedByMemberId,
|
|
885
1179
|
manager,
|
|
@@ -930,13 +1224,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
930
1224
|
return decision;
|
|
931
1225
|
}
|
|
932
1226
|
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED) {
|
|
933
|
-
await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt);
|
|
1227
|
+
await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt, input.decidedByMemberId);
|
|
934
1228
|
return decision;
|
|
935
1229
|
}
|
|
936
1230
|
if (await this.handleAdhocPreApprovalRejection(manager, instance, completedTask, decidedAt, decisionComment)) {
|
|
937
1231
|
return decision;
|
|
938
1232
|
}
|
|
939
|
-
await this.rejectInstance(manager, instance, completedTask, decidedAt);
|
|
1233
|
+
await this.rejectInstance(manager, instance, completedTask, decidedAt, input.decidedByMemberId);
|
|
940
1234
|
return decision;
|
|
941
1235
|
});
|
|
942
1236
|
}
|
|
@@ -970,6 +1264,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
970
1264
|
},
|
|
971
1265
|
taskId: null,
|
|
972
1266
|
}));
|
|
1267
|
+
await this.recordInstanceEvent(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED, 'INSTANCE_CANCELLED', { actorMemberId: input.cancelledByMemberId });
|
|
973
1268
|
return cancelledInstance;
|
|
974
1269
|
});
|
|
975
1270
|
}
|
|
@@ -1007,6 +1302,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1007
1302
|
},
|
|
1008
1303
|
taskId: input.taskId ?? null,
|
|
1009
1304
|
}));
|
|
1305
|
+
await this.recordInstanceEvent(manager, expiredInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.EXPIRED, 'INSTANCE_EXPIRED', {});
|
|
1010
1306
|
return expiredInstance;
|
|
1011
1307
|
});
|
|
1012
1308
|
}
|
|
@@ -1036,6 +1332,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1036
1332
|
}
|
|
1037
1333
|
async resubmitApprovalInstance(input, authContext) {
|
|
1038
1334
|
const submittedFormData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
1335
|
+
if (input.endUserSubmission === true) {
|
|
1336
|
+
await this.assertEndUserMayResubmit(input.instanceId);
|
|
1337
|
+
}
|
|
1039
1338
|
const preparation = this.formDataSourceValueResolver
|
|
1040
1339
|
? await this.prepareResubmitFormDataOptions(input, submittedFormData, authContext ?? createWorkflowAuthContext(input.initiatorMemberId))
|
|
1041
1340
|
: null;
|
|
@@ -1065,20 +1364,37 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1065
1364
|
throw new common_1.ConflictException('Approval instance changed while options were resolving; refresh and retry');
|
|
1066
1365
|
}
|
|
1067
1366
|
const resubmitSchema = readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot);
|
|
1068
|
-
const
|
|
1367
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(instance.workflowSnapshot);
|
|
1069
1368
|
// An end user gets back the value an approver already filled in, and
|
|
1070
1369
|
// loses a key the stored data never had: the renderer seeds `false`,
|
|
1071
1370
|
// `[]` and empty rows for every field, none of which an approver wrote.
|
|
1371
|
+
// An open cell of a table is settled per row the same way.
|
|
1072
1372
|
const formData = input.endUserSubmission === true
|
|
1073
|
-
? (0, bpm_core_shared_1.overlayStoredFormDataKeys)(submittedFormData, instance.formData,
|
|
1373
|
+
? (0, approver_filled_table_columns_1.applyOpenColumnCells)((0, bpm_core_shared_1.overlayStoredFormDataKeys)(submittedFormData, instance.formData, deferral.wholeKeys), instance.formData, deferral)
|
|
1074
1374
|
: submittedFormData;
|
|
1075
|
-
validateSubmittedFormData(resubmitSchema, formData, {
|
|
1076
|
-
deferredFieldKeys,
|
|
1077
|
-
});
|
|
1375
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(resubmitSchema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
1078
1376
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData, manager);
|
|
1079
1377
|
if (!preparation) {
|
|
1080
1378
|
assertNoUnresolvableDynamicValues(resubmitSchema, formData);
|
|
1081
1379
|
}
|
|
1380
|
+
// ADR 27 §2: the host guard judges the resubmission as it will be
|
|
1381
|
+
// committed, after every form check and before the write.
|
|
1382
|
+
if (this.instanceGuard !== null) {
|
|
1383
|
+
await this.guardInstanceChange({
|
|
1384
|
+
action: null,
|
|
1385
|
+
actorMemberId: input.initiatorMemberId,
|
|
1386
|
+
authContext: authContext ?? null,
|
|
1387
|
+
automatic: null,
|
|
1388
|
+
changedFieldKeys: listChangedFormDataKeys(instance.formData, formData),
|
|
1389
|
+
formData,
|
|
1390
|
+
instance,
|
|
1391
|
+
kind: 'RESUBMIT',
|
|
1392
|
+
manager,
|
|
1393
|
+
node: null,
|
|
1394
|
+
task: null,
|
|
1395
|
+
templateVersion: await this.readBoundTemplateVersionOrThrow(manager, instance),
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1082
1398
|
const resubmittedInstance = await instanceRepository.save({
|
|
1083
1399
|
...instance,
|
|
1084
1400
|
completedAt: null,
|
|
@@ -1091,6 +1407,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1091
1407
|
state: workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING,
|
|
1092
1408
|
title: input.title?.trim() || instance.title,
|
|
1093
1409
|
});
|
|
1410
|
+
// Written before the resume below, so RESUBMITTED precedes any
|
|
1411
|
+
// NODE_ACTIVATED that the resume produces in the same transaction.
|
|
1412
|
+
await this.recordInstanceEvent(manager, resubmittedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING, 'INSTANCE_RESUBMITTED', { actorMemberId: input.initiatorMemberId });
|
|
1094
1413
|
await this.attachmentService.bindFormDataAttachmentsToInstance(manager, {
|
|
1095
1414
|
formData,
|
|
1096
1415
|
instanceId: instance.id,
|
|
@@ -1864,11 +2183,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1864
2183
|
input.workflowDefinitionJson,
|
|
1865
2184
|
input.formDataJson,
|
|
1866
2185
|
input.initiatorMetadataSnapshotJson ?? '',
|
|
2186
|
+
input.formSchemaJson ?? '',
|
|
1867
2187
|
].some((json) => json.length > MAX_DRY_RUN_INPUT_LENGTH)) {
|
|
1868
2188
|
return createFailedDryRunResult('試跑資料過大,請縮小流程或測試資料後再試');
|
|
1869
2189
|
}
|
|
1870
2190
|
const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
|
|
1871
2191
|
const formData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
2192
|
+
const formSchema = readDryRunFormSchema(input.formSchemaJson);
|
|
1872
2193
|
// The snapshot here is the caller's own sample (the designer always sends
|
|
1873
2194
|
// one) or a bare `{ customFields: {}, memberId }`. It is not the one a real
|
|
1874
2195
|
// submission builds, and a dry run has no template version to hand the
|
|
@@ -1885,6 +2206,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1885
2206
|
return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
|
|
1886
2207
|
errors: [],
|
|
1887
2208
|
steps: (0, expression_budget_1.runExpressionOperation)(() => (0, time_budget_1.runWithinTimeBudget)(() => this.simulateWorkflow({
|
|
2209
|
+
expressionFormData: this.expressionMissingFormFieldsAsNull && formSchema
|
|
2210
|
+
? (0, missing_form_fields_1.fillMissingFormFields)(formData, formSchema)
|
|
2211
|
+
: formData,
|
|
1888
2212
|
formData,
|
|
1889
2213
|
initiatorMemberId: input.initiatorMemberId,
|
|
1890
2214
|
initiatorMetadataSnapshot,
|
|
@@ -2189,6 +2513,51 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2189
2513
|
}
|
|
2190
2514
|
return instance;
|
|
2191
2515
|
}
|
|
2516
|
+
/**
|
|
2517
|
+
* ADR 27 §4.2 (gap 1): the fill units an approver may use to resolve runtime
|
|
2518
|
+
* data-source options on a RUNNING instance. Only tasks the member can act on
|
|
2519
|
+
* count: the member is the assignee or a PENDING/CLAIMED candidate of a
|
|
2520
|
+
* PENDING or IN_PROGRESS, non-ad-hoc task. A task whose candidate row is
|
|
2521
|
+
* WAITING or already decided grants nothing, even though its status is open.
|
|
2522
|
+
* A field counts only when its node lists it as SHOW; a HIDE entry never
|
|
2523
|
+
* grants, whatever its fill is. Returns `[]` when the instance is not RUNNING.
|
|
2524
|
+
*/
|
|
2525
|
+
async listApproverFillUnits(instanceId, memberId) {
|
|
2526
|
+
const instance = await this.approvalInstanceRepository.findOne({
|
|
2527
|
+
where: { id: instanceId },
|
|
2528
|
+
});
|
|
2529
|
+
if (!instance ||
|
|
2530
|
+
instance.state !== workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
|
|
2531
|
+
return [];
|
|
2532
|
+
}
|
|
2533
|
+
const tasks = await this.taskRepository.find({
|
|
2534
|
+
where: {
|
|
2535
|
+
instanceId,
|
|
2536
|
+
isAdhoc: false,
|
|
2537
|
+
status: (0, typeorm_2.In)([workflow_engine_enums_1.TaskStatusEnum.PENDING, workflow_engine_enums_1.TaskStatusEnum.IN_PROGRESS]),
|
|
2538
|
+
},
|
|
2539
|
+
});
|
|
2540
|
+
const unitsPerTask = await Promise.all(tasks.map(async (task) => {
|
|
2541
|
+
const node = instance.workflowSnapshot.nodes.find((candidate) => candidate.type === 'userTask' && candidate.id === task.nodeId);
|
|
2542
|
+
if (!node) {
|
|
2543
|
+
return [];
|
|
2544
|
+
}
|
|
2545
|
+
try {
|
|
2546
|
+
await this.readTaskActorContext(this.taskRepository.manager, task, memberId);
|
|
2547
|
+
}
|
|
2548
|
+
catch (error) {
|
|
2549
|
+
if (error instanceof common_1.ConflictException) {
|
|
2550
|
+
return [];
|
|
2551
|
+
}
|
|
2552
|
+
throw error;
|
|
2553
|
+
}
|
|
2554
|
+
const shown = new Set((0, bpm_core_shared_1.readNodeFieldAccess)(node)
|
|
2555
|
+
.filter((entry) => entry.display === 'SHOW')
|
|
2556
|
+
.map((entry) => entry.fieldKey));
|
|
2557
|
+
return (0, bpm_core_shared_1.readNodeFillUnits)(node).filter((unit) => shown.has(unit.fieldKey));
|
|
2558
|
+
}));
|
|
2559
|
+
return unitsPerTask.flat();
|
|
2560
|
+
}
|
|
2192
2561
|
/**
|
|
2193
2562
|
* ADR 25 §6: a value written to a filterable field must fit its type, on
|
|
2194
2563
|
* every path that writes form data. The fields checked are the ones the
|
|
@@ -2274,7 +2643,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2274
2643
|
totalPages,
|
|
2275
2644
|
};
|
|
2276
2645
|
}
|
|
2277
|
-
async createFilteredApprovalInstanceQuery(scope,
|
|
2646
|
+
async createFilteredApprovalInstanceQuery(scope, requested = {}) {
|
|
2647
|
+
const options = await this.resolveApprovalInstanceTemplateKey(requested);
|
|
2648
|
+
if (!options) {
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2278
2651
|
const queryBuilder = this.approvalInstanceRepository.createQueryBuilder('approvalInstance');
|
|
2279
2652
|
const hasReadableInstances = this.applyReadableInstanceFilter(queryBuilder, scope, options);
|
|
2280
2653
|
if (!hasReadableInstances) {
|
|
@@ -2502,9 +2875,15 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2502
2875
|
memberId,
|
|
2503
2876
|
submission: null,
|
|
2504
2877
|
});
|
|
2505
|
-
return templates.filter((template) =>
|
|
2506
|
-
|
|
2507
|
-
|
|
2878
|
+
return templates.filter((template) => {
|
|
2879
|
+
const version = template.currentVersionId
|
|
2880
|
+
? (versionById.get(template.currentVersionId) ?? null)
|
|
2881
|
+
: null;
|
|
2882
|
+
// A HOST_ONLY template is started by the host's backend, never from the
|
|
2883
|
+
// launch page (ADR 27 §5), so it is not offered there at all.
|
|
2884
|
+
return (version?.launchMode !== template_enums_1.ApprovalTemplateLaunchModeEnum.HOST_ONLY &&
|
|
2885
|
+
this.isInitiatorPolicyMet(template, version, initiatorMetadataSnapshot, 'launchable templates'));
|
|
2886
|
+
});
|
|
2508
2887
|
}
|
|
2509
2888
|
/**
|
|
2510
2889
|
* The published version whose form `memberId` may read data-source options
|
|
@@ -2605,7 +2984,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2605
2984
|
* given its candidate summaries.
|
|
2606
2985
|
*/
|
|
2607
2986
|
async listInboxTasks(assigneeMemberId, options = {}) {
|
|
2608
|
-
const openTasks = await this.readSharedOpenInboxTasks(assigneeMemberId);
|
|
2987
|
+
const openTasks = await this.filterTasksByTemplate(await this.readSharedOpenInboxTasks(assigneeMemberId), options);
|
|
2609
2988
|
if (!isTaskListPaged(options)) {
|
|
2610
2989
|
return this.attachTaskCandidateSummaries(openTasks);
|
|
2611
2990
|
}
|
|
@@ -2613,8 +2992,77 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2613
2992
|
return this.attachTaskCandidateSummaries(openTasks.slice(offset, offset + normalizePageSize(options.pageSize)));
|
|
2614
2993
|
}
|
|
2615
2994
|
/** How many tasks {@link listInboxTasks} pages through. */
|
|
2616
|
-
async countInboxTasks(assigneeMemberId) {
|
|
2617
|
-
return (await this.readSharedOpenInboxTasks(assigneeMemberId)).length;
|
|
2995
|
+
async countInboxTasks(assigneeMemberId, filter = {}) {
|
|
2996
|
+
return (await this.filterTasksByTemplate(await this.readSharedOpenInboxTasks(assigneeMemberId), filter)).length;
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* The template ids a {@link TaskTemplateFilter} allows, or `null` when it
|
|
3000
|
+
* does not filter. A `templateKey` that names no template allows nothing.
|
|
3001
|
+
*/
|
|
3002
|
+
async readTaskTemplateFilterIds(filter) {
|
|
3003
|
+
// Trimmed, and a blank key does not filter, as on the instance list.
|
|
3004
|
+
const key = normalizeText(filter.templateKey);
|
|
3005
|
+
const byKey = key ? await this.readTemplateIdsByKey(key) : null;
|
|
3006
|
+
if (filter.templateIds === undefined) {
|
|
3007
|
+
return byKey;
|
|
3008
|
+
}
|
|
3009
|
+
if (byKey === null) {
|
|
3010
|
+
return filter.templateIds;
|
|
3011
|
+
}
|
|
3012
|
+
return filter.templateIds.filter((id) => byKey.includes(id));
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Turns `templateKey` into `templateId` (ADR 27 §6), or `null` when the
|
|
3016
|
+
* options can match nothing: an unknown key, or a key naming another
|
|
3017
|
+
* template than `templateId` does.
|
|
3018
|
+
*/
|
|
3019
|
+
async resolveApprovalInstanceTemplateKey(options) {
|
|
3020
|
+
const key = normalizeText(options.templateKey);
|
|
3021
|
+
if (!key) {
|
|
3022
|
+
return options;
|
|
3023
|
+
}
|
|
3024
|
+
const [templateId] = await this.readTemplateIdsByKey(key);
|
|
3025
|
+
const requestedId = normalizeText(options.templateId);
|
|
3026
|
+
if (!templateId || (requestedId && requestedId !== templateId)) {
|
|
3027
|
+
return null;
|
|
3028
|
+
}
|
|
3029
|
+
return { ...options, templateId };
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* The id of the live template carrying `key`, as a list so a key that names
|
|
3033
|
+
* nothing filters everything out. At most one live template holds a key.
|
|
3034
|
+
*/
|
|
3035
|
+
async readTemplateIdsByKey(key) {
|
|
3036
|
+
const template = await this.approvalTemplateRepository.findOne({
|
|
3037
|
+
select: { id: true },
|
|
3038
|
+
where: { key },
|
|
3039
|
+
});
|
|
3040
|
+
return template ? [template.id] : [];
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Keeps the tasks whose instance belongs to the filter's templates. The open
|
|
3044
|
+
* task read is shared with the dashboard counters, so the filter is applied
|
|
3045
|
+
* to its result rather than pushed into it.
|
|
3046
|
+
*/
|
|
3047
|
+
async filterTasksByTemplate(tasks, filter) {
|
|
3048
|
+
const templateIds = await this.readTaskTemplateFilterIds(filter);
|
|
3049
|
+
if (templateIds === null) {
|
|
3050
|
+
return tasks;
|
|
3051
|
+
}
|
|
3052
|
+
if (!templateIds.length || !tasks.length) {
|
|
3053
|
+
return [];
|
|
3054
|
+
}
|
|
3055
|
+
const instances = await this.approvalInstanceRepository.find({
|
|
3056
|
+
select: { id: true },
|
|
3057
|
+
where: {
|
|
3058
|
+
id: (0, typeorm_2.In)([
|
|
3059
|
+
...new Set(tasks.map((task) => task.instanceId)),
|
|
3060
|
+
]),
|
|
3061
|
+
templateId: (0, typeorm_2.In)([...templateIds]),
|
|
3062
|
+
},
|
|
3063
|
+
});
|
|
3064
|
+
const instanceIds = new Set(instances.map((instance) => instance.id));
|
|
3065
|
+
return tasks.filter((task) => instanceIds.has(task.instanceId));
|
|
2618
3066
|
}
|
|
2619
3067
|
/**
|
|
2620
3068
|
* {@link readOpenInboxTasks} for the inbox list and its count, joining a
|
|
@@ -2711,7 +3159,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2711
3159
|
*/
|
|
2712
3160
|
async listApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
2713
3161
|
const paged = isTaskListPaged(options);
|
|
2714
|
-
const
|
|
3162
|
+
const templateIds = await this.readTaskTemplateFilterIds(options);
|
|
3163
|
+
if (templateIds?.length === 0) {
|
|
3164
|
+
return [];
|
|
3165
|
+
}
|
|
3166
|
+
const tasks = await this.createApprovalHistoryTaskQuery(assigneeMemberId, options.instanceState, templateIds)
|
|
2715
3167
|
.orderBy(this.readApprovalHistoryActedAtExpression(), 'DESC')
|
|
2716
3168
|
.addOrderBy('"task"."id"', 'DESC')
|
|
2717
3169
|
.offset(paged ? readPageOffset(options.page, options.pageSize) : 0)
|
|
@@ -2723,10 +3175,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2723
3175
|
}
|
|
2724
3176
|
/** How many tasks {@link listApprovalHistoryTasks} pages through. */
|
|
2725
3177
|
async countApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
2726
|
-
|
|
3178
|
+
const templateIds = await this.readTaskTemplateFilterIds(options);
|
|
3179
|
+
if (templateIds?.length === 0) {
|
|
3180
|
+
return 0;
|
|
3181
|
+
}
|
|
3182
|
+
return this.createApprovalHistoryTaskQuery(assigneeMemberId, options.instanceState, templateIds).getCount();
|
|
2727
3183
|
}
|
|
2728
3184
|
/** The rows of {@link listApprovalHistoryTasks}, unordered and unpaged. */
|
|
2729
|
-
createApprovalHistoryTaskQuery(assigneeMemberId, instanceState) {
|
|
3185
|
+
createApprovalHistoryTaskQuery(assigneeMemberId, instanceState, templateIds = null) {
|
|
2730
3186
|
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
2731
3187
|
const queryBuilder = this.taskRepository
|
|
2732
3188
|
.createQueryBuilder('task')
|
|
@@ -2759,6 +3215,16 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2759
3215
|
)
|
|
2760
3216
|
`, { historyInstanceState: instanceState });
|
|
2761
3217
|
}
|
|
3218
|
+
if (templateIds) {
|
|
3219
|
+
queryBuilder.andWhere(`
|
|
3220
|
+
EXISTS (
|
|
3221
|
+
SELECT 1
|
|
3222
|
+
FROM ${this.readQualifiedTableName(this.approvalInstanceRepository)} "historyTemplateInstance"
|
|
3223
|
+
WHERE "historyTemplateInstance"."id" = "task"."instance_id"
|
|
3224
|
+
AND "historyTemplateInstance"."template_id" IN (:...historyTemplateIds)
|
|
3225
|
+
)
|
|
3226
|
+
`, { historyTemplateIds: [...templateIds] });
|
|
3227
|
+
}
|
|
2762
3228
|
return queryBuilder;
|
|
2763
3229
|
}
|
|
2764
3230
|
/** When the member acted on a history task; see {@link listApprovalHistoryTasks}. */
|
|
@@ -2845,7 +3311,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2845
3311
|
* snapshot without a `nodes` array, and a blank or non-string label all
|
|
2846
3312
|
* leave the task without a label, as `readWorkflowNodeLabel` does. Only the
|
|
2847
3313
|
* node's `fieldAccess` leaves the database for the fill flag, which
|
|
2848
|
-
* `
|
|
3314
|
+
* `readNodeFillGrantFieldKeys` then reads as tolerantly as the engine does.
|
|
2849
3315
|
*/
|
|
2850
3316
|
async readTaskNodeSummaries(tasks) {
|
|
2851
3317
|
// The table as the entity reads it, so a configured `schema` is honoured
|
|
@@ -2880,7 +3346,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2880
3346
|
readNodeLabelKey(row.instanceId, row.nodeId),
|
|
2881
3347
|
{
|
|
2882
3348
|
hasFillableFields: row.nodeType === 'userTask' &&
|
|
2883
|
-
(0, bpm_core_shared_1.
|
|
3349
|
+
(0, bpm_core_shared_1.readNodeFillGrantFieldKeys)({
|
|
2884
3350
|
data: { fieldAccess: row.fieldAccess },
|
|
2885
3351
|
}).size > 0,
|
|
2886
3352
|
label: typeof row.label === 'string' && row.label.trim()
|
|
@@ -3169,6 +3635,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3169
3635
|
});
|
|
3170
3636
|
await this.dispatchAdhocCompletionNotifications(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED);
|
|
3171
3637
|
await this.cancelPendingAdhocDirectives(manager, instance.id, null);
|
|
3638
|
+
await this.recordInstanceEvent(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED, 'INSTANCE_APPROVED', {});
|
|
3172
3639
|
}
|
|
3173
3640
|
async isNodeEntryReady(manager, instance, token, node) {
|
|
3174
3641
|
if (node.type === 'startEvent') {
|
|
@@ -3234,7 +3701,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3234
3701
|
}
|
|
3235
3702
|
isNodeEntryConditionMet(instance, node) {
|
|
3236
3703
|
const entryCondition = readNodeEntryCondition(node);
|
|
3237
|
-
return this.conditionService.evaluateBoolean(entryCondition,
|
|
3704
|
+
return this.conditionService.evaluateBoolean(entryCondition, this.buildExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`, readExpressionDialectOptions(entryCondition, instance.workflowSnapshot));
|
|
3238
3705
|
}
|
|
3239
3706
|
async skipNodeByEntryCondition(manager, instance, token, node) {
|
|
3240
3707
|
await this.skipNode(manager, instance, token, node, {
|
|
@@ -3308,7 +3775,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3308
3775
|
});
|
|
3309
3776
|
}
|
|
3310
3777
|
const context = {
|
|
3311
|
-
...
|
|
3778
|
+
...this.buildExpressionContext(instance),
|
|
3312
3779
|
lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
|
|
3313
3780
|
};
|
|
3314
3781
|
return evaluateWithStructuredFallback(edge, instance.formData, (message) => this.logger.warn(`instance=${instance.id} ${message}`), () => this.conditionService.evaluateBoolean(expression, context, `workflow.edges.${edge.id}.data.condition`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) }));
|
|
@@ -3431,6 +3898,12 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3431
3898
|
// worse — an `endState: 'REJECTED'` row overwritten with `APPROVED`
|
|
3432
3899
|
// immediately after it had been committed.
|
|
3433
3900
|
const completedInstance = await instanceRepository.save(Object.assign(instance, { completedAt, state: instanceState }));
|
|
3901
|
+
if (instanceState === workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED) {
|
|
3902
|
+
await this.recordInstanceEvent(manager, completedInstance, instanceState, 'INSTANCE_REJECTED', { actorMemberId: null });
|
|
3903
|
+
}
|
|
3904
|
+
else {
|
|
3905
|
+
await this.recordInstanceEvent(manager, completedInstance, instanceState, 'INSTANCE_APPROVED', {});
|
|
3906
|
+
}
|
|
3434
3907
|
if (instanceState === workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED) {
|
|
3435
3908
|
await this.notificationService.createInstanceCompletedNotification({
|
|
3436
3909
|
instance: completedInstance,
|
|
@@ -3532,7 +4005,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3532
4005
|
const singleCandidate = savedCandidates.length === 1 ? savedCandidates[0] : null;
|
|
3533
4006
|
const openCandidates = savedCandidates.filter((candidate) => candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING);
|
|
3534
4007
|
task.openCandidateMemberIds = openCandidates.map((candidate) => candidate.memberId);
|
|
3535
|
-
task.hasFillableFields = (0, bpm_core_shared_1.
|
|
4008
|
+
task.hasFillableFields = (0, bpm_core_shared_1.readNodeFillGrantFieldKeys)(node).size > 0;
|
|
3536
4009
|
await tokenRepository.save({
|
|
3537
4010
|
...token,
|
|
3538
4011
|
status: workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING,
|
|
@@ -3559,6 +4032,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3559
4032
|
},
|
|
3560
4033
|
taskId: task.id,
|
|
3561
4034
|
}));
|
|
4035
|
+
await this.recordInstanceEvent(manager, instance, instance.state, 'NODE_ACTIVATED', { nodeId: node.id, taskIds: [task.id] });
|
|
3562
4036
|
await openCandidates.reduce(async (previous, candidate) => {
|
|
3563
4037
|
await previous;
|
|
3564
4038
|
await this.notificationService.createTaskAssignedNotification({
|
|
@@ -4618,9 +5092,22 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4618
5092
|
skippedDelegations: collectSkippedDelegations(delegatedCandidates, true, replacementExcludedMemberIds),
|
|
4619
5093
|
};
|
|
4620
5094
|
}
|
|
5095
|
+
/**
|
|
5096
|
+
* The context workflow expressions on `instance` read. Its `form` is the
|
|
5097
|
+
* stored data, or with `expressionMissingFormFieldsAsNull` on, a copy with
|
|
5098
|
+
* every unfilled top-level field present (ADR 27 §3).
|
|
5099
|
+
*/
|
|
5100
|
+
buildExpressionContext(instance) {
|
|
5101
|
+
return buildWorkflowExpressionContext(instance, this.readExpressionFormData(instance));
|
|
5102
|
+
}
|
|
5103
|
+
readExpressionFormData(instance) {
|
|
5104
|
+
return this.expressionMissingFormFieldsAsNull
|
|
5105
|
+
? (0, missing_form_fields_1.fillMissingFormFields)(instance.formData, readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot))
|
|
5106
|
+
: instance.formData;
|
|
5107
|
+
}
|
|
4621
5108
|
buildDelegationContext(instance, nodeId) {
|
|
4622
5109
|
return {
|
|
4623
|
-
formData: instance
|
|
5110
|
+
formData: this.readExpressionFormData(instance),
|
|
4624
5111
|
initiatorMemberId: instance.initiatorMemberId,
|
|
4625
5112
|
initiatorMetadataSnapshot: instance.initiatorMetadataSnapshot,
|
|
4626
5113
|
instanceId: instance.id,
|
|
@@ -4714,7 +5201,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4714
5201
|
return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`);
|
|
4715
5202
|
}
|
|
4716
5203
|
if (resolver.type === 'EXPRESSION') {
|
|
4717
|
-
return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression,
|
|
5204
|
+
return readMemberIdsFromValue(this.conditionService.evaluateValue(resolver.expression, this.buildExpressionContext(instance), `${label}.expression`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) }), label, subject, '依運算式');
|
|
4718
5205
|
}
|
|
4719
5206
|
if (resolver.type === 'POSITION') {
|
|
4720
5207
|
return this.resolvePositionCandidates(manager, resolver.positionId, label, subject);
|
|
@@ -4844,7 +5331,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4844
5331
|
*/
|
|
4845
5332
|
async readNodeActorApprovals(manager, instance, nodeIds) {
|
|
4846
5333
|
const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
|
|
4847
|
-
where: {
|
|
5334
|
+
where: {
|
|
5335
|
+
instanceId: instance.id,
|
|
5336
|
+
isAdhoc: false,
|
|
5337
|
+
nodeId: (0, typeorm_2.In)([...nodeIds]),
|
|
5338
|
+
},
|
|
4848
5339
|
});
|
|
4849
5340
|
if (tasks.length === 0) {
|
|
4850
5341
|
return [];
|
|
@@ -4860,7 +5351,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4860
5351
|
}
|
|
4861
5352
|
const [decisions, decidedLogs] = await Promise.all([
|
|
4862
5353
|
manager.getRepository(task_decision_entity_1.TaskDecisionEntity).find({
|
|
4863
|
-
where: {
|
|
5354
|
+
where: {
|
|
5355
|
+
action: workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED,
|
|
5356
|
+
taskId: (0, typeorm_2.In)(batchTaskIds),
|
|
5357
|
+
},
|
|
4864
5358
|
}),
|
|
4865
5359
|
manager.getRepository(activity_log_entity_1.ActivityLogEntity).find({
|
|
4866
5360
|
where: {
|
|
@@ -5322,11 +5816,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5322
5816
|
const action = node.data.action;
|
|
5323
5817
|
if (action.type === 'WEBHOOK') {
|
|
5324
5818
|
const payload = typeof action.payload === 'string' && action.payload.trim()
|
|
5325
|
-
? this.conditionService.evaluateValue(action.payload,
|
|
5819
|
+
? this.conditionService.evaluateValue(action.payload, this.buildExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`, {
|
|
5326
5820
|
dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot),
|
|
5327
5821
|
})
|
|
5328
5822
|
: {
|
|
5329
|
-
instance:
|
|
5823
|
+
instance: this.buildExpressionContext(instance).instance,
|
|
5330
5824
|
triggeredAt: new Date().toISOString(),
|
|
5331
5825
|
};
|
|
5332
5826
|
await this.dispatchWebhookAfterCommit(manager, {
|
|
@@ -5354,7 +5848,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5354
5848
|
return;
|
|
5355
5849
|
}
|
|
5356
5850
|
if (action.type === 'SET_FORM_FIELD') {
|
|
5357
|
-
const value = this.conditionService.evaluateValue(action.value,
|
|
5851
|
+
const value = this.conditionService.evaluateValue(action.value, this.buildExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) });
|
|
5358
5852
|
assertServiceTaskFieldValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
|
|
5359
5853
|
const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
|
|
5360
5854
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, updatedFormData, manager, node.data.label);
|
|
@@ -5416,8 +5910,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5416
5910
|
// §3.5). Without the webhook module there is nothing to queue into; the
|
|
5417
5911
|
// publish lint already refuses a template that references an endpoint
|
|
5418
5912
|
// in that case.
|
|
5419
|
-
const webhookDeliveryIds = this.webhookDeliveryService &&
|
|
5420
|
-
(0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
|
|
5913
|
+
const webhookDeliveryIds = this.webhookDeliveryService && (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
|
|
5421
5914
|
? await this.webhookDeliveryService.enqueueNotifyWebhooks(manager, {
|
|
5422
5915
|
instance: {
|
|
5423
5916
|
formData: instance.formData,
|
|
@@ -5447,7 +5940,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5447
5940
|
taskId: null,
|
|
5448
5941
|
}));
|
|
5449
5942
|
}
|
|
5450
|
-
async rejectInstance(manager, instance, task, rejectedAt) {
|
|
5943
|
+
async rejectInstance(manager, instance, task, rejectedAt, decidedByMemberId) {
|
|
5451
5944
|
const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
|
|
5452
5945
|
// Dispatch pending ad-hoc notifications for the rejecting node before the
|
|
5453
5946
|
// remaining directives are cancelled by the terminal cleanup below.
|
|
@@ -5478,8 +5971,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5478
5971
|
},
|
|
5479
5972
|
taskId: task.id,
|
|
5480
5973
|
}));
|
|
5974
|
+
await this.recordInstanceEvent(manager, rejectedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED, 'INSTANCE_REJECTED', { actorMemberId: decidedByMemberId });
|
|
5481
5975
|
}
|
|
5482
|
-
async returnInstanceToNode(manager, instance, task, returnToNodeId, returnedAt) {
|
|
5976
|
+
async returnInstanceToNode(manager, instance, task, returnToNodeId, returnedAt, decidedByMemberId) {
|
|
5483
5977
|
if (!returnToNodeId) {
|
|
5484
5978
|
throw new common_1.ConflictException('Return target node is required');
|
|
5485
5979
|
}
|
|
@@ -5537,6 +6031,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5537
6031
|
taskId: null,
|
|
5538
6032
|
}),
|
|
5539
6033
|
]);
|
|
6034
|
+
await this.recordInstanceEvent(manager, returnedInstance, returnedInstance.state, 'INSTANCE_RETURNED', { actorMemberId: decidedByMemberId, nodeId: task.nodeId });
|
|
5540
6035
|
if (returnedInstance.state === workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
|
|
5541
6036
|
await this.processRunningInstance(manager, returnedInstance);
|
|
5542
6037
|
}
|
|
@@ -5738,6 +6233,9 @@ exports.WorkflowEngineService = WorkflowEngineService = WorkflowEngineService_1
|
|
|
5738
6233
|
tslib_1.__param(20, (0, common_1.Optional)()),
|
|
5739
6234
|
tslib_1.__param(21, (0, common_1.Optional)()),
|
|
5740
6235
|
tslib_1.__param(21, (0, common_1.Inject)(initiator_custom_fields_resolver_token_1.BPM_INITIATOR_CUSTOM_FIELDS_RESOLVER)),
|
|
6236
|
+
tslib_1.__param(22, (0, common_1.Optional)()),
|
|
6237
|
+
tslib_1.__param(22, (0, common_1.Inject)(instance_guard_token_1.BPM_INSTANCE_GUARD)),
|
|
6238
|
+
tslib_1.__param(23, (0, common_1.Optional)()),
|
|
5741
6239
|
tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
5742
6240
|
typeorm_2.Repository,
|
|
5743
6241
|
typeorm_2.Repository,
|
|
@@ -5754,7 +6252,7 @@ exports.WorkflowEngineService = WorkflowEngineService = WorkflowEngineService_1
|
|
|
5754
6252
|
delegation_service_1.DelegationService,
|
|
5755
6253
|
notification_service_1.NotificationService,
|
|
5756
6254
|
signature_service_1.SignatureService,
|
|
5757
|
-
sla_schedule_service_1.BPMSlaScheduleService, Object, Object, Object, workflow_webhook_delivery_service_1.WorkflowWebhookDeliveryService, Object])
|
|
6255
|
+
sla_schedule_service_1.BPMSlaScheduleService, Object, Object, Object, workflow_webhook_delivery_service_1.WorkflowWebhookDeliveryService, Object, Object, instance_event_writer_1.InstanceEventWriter])
|
|
5758
6256
|
], WorkflowEngineService);
|
|
5759
6257
|
function parseJsonObject(value, label) {
|
|
5760
6258
|
try {
|
|
@@ -5777,6 +6275,67 @@ function readDefaultInitiatorMetadataSnapshot(memberId) {
|
|
|
5777
6275
|
memberId,
|
|
5778
6276
|
};
|
|
5779
6277
|
}
|
|
6278
|
+
/**
|
|
6279
|
+
* The form schema a dry run was given to fill unfilled fields with (ADR 27
|
|
6280
|
+
* §3), or `null` when it was given none. A schema that does not parse is a
|
|
6281
|
+
* 400, like the other inputs.
|
|
6282
|
+
*/
|
|
6283
|
+
function readDryRunFormSchema(json) {
|
|
6284
|
+
if (!json) {
|
|
6285
|
+
return null;
|
|
6286
|
+
}
|
|
6287
|
+
const schema = parseJsonObject(json, 'formSchemaJson');
|
|
6288
|
+
if (!isFormDefinitionSchema(schema)) {
|
|
6289
|
+
throw new common_1.BadRequestException('formSchemaJson is not a form schema');
|
|
6290
|
+
}
|
|
6291
|
+
return schema;
|
|
6292
|
+
}
|
|
6293
|
+
/**
|
|
6294
|
+
* Refuses a submission or resubmission through GraphQL on a `HOST_ONLY`
|
|
6295
|
+
* version (ADR 27 §5). Only the host's backend, calling the service, may start
|
|
6296
|
+
* or resubmit such a case.
|
|
6297
|
+
*/
|
|
6298
|
+
function assertEndUserMayLaunch(version) {
|
|
6299
|
+
if (version.launchMode === template_enums_1.ApprovalTemplateLaunchModeEnum.HOST_ONLY) {
|
|
6300
|
+
throw new common_1.ForbiddenException('This approval template can only be submitted by the host system');
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
/**
|
|
6304
|
+
* Reads the service-only `initiatorCustomFields` of a submission (ADR 27 §5):
|
|
6305
|
+
* `null` when absent, otherwise a JSON copy. Refused with 400 when it is not a
|
|
6306
|
+
* plain object or comes together with a whole `initiatorMetadataSnapshotJson`,
|
|
6307
|
+
* which would leave it unclear which one decides.
|
|
6308
|
+
*/
|
|
6309
|
+
function readHostInitiatorCustomFields(input) {
|
|
6310
|
+
const fields = input.initiatorCustomFields;
|
|
6311
|
+
if (fields === undefined || fields === null) {
|
|
6312
|
+
return null;
|
|
6313
|
+
}
|
|
6314
|
+
if (input.initiatorMetadataSnapshotJson) {
|
|
6315
|
+
throw new common_1.BadRequestException('initiatorCustomFields cannot be combined with initiatorMetadataSnapshotJson');
|
|
6316
|
+
}
|
|
6317
|
+
if (typeof fields !== 'object' || Array.isArray(fields)) {
|
|
6318
|
+
throw new common_1.BadRequestException('initiatorCustomFields must be an object');
|
|
6319
|
+
}
|
|
6320
|
+
return cloneJsonObject(fields);
|
|
6321
|
+
}
|
|
6322
|
+
/**
|
|
6323
|
+
* Lays the host's `initiatorCustomFields` over the `customFields` of a built
|
|
6324
|
+
* snapshot; a key the host gave wins over the resolver's.
|
|
6325
|
+
*/
|
|
6326
|
+
function mergeHostInitiatorCustomFields(snapshot, hostCustomFields) {
|
|
6327
|
+
if (hostCustomFields === null) {
|
|
6328
|
+
return snapshot;
|
|
6329
|
+
}
|
|
6330
|
+
const current = snapshot['customFields'];
|
|
6331
|
+
return {
|
|
6332
|
+
...snapshot,
|
|
6333
|
+
customFields: {
|
|
6334
|
+
...(isRecord(current) ? current : {}),
|
|
6335
|
+
...hostCustomFields,
|
|
6336
|
+
},
|
|
6337
|
+
};
|
|
6338
|
+
}
|
|
5780
6339
|
function cloneJsonObject(value) {
|
|
5781
6340
|
return JSON.parse(JSON.stringify(value));
|
|
5782
6341
|
}
|
|
@@ -5861,7 +6420,8 @@ function createWorkflowAuthContext(memberId) {
|
|
|
5861
6420
|
};
|
|
5862
6421
|
}
|
|
5863
6422
|
function isSameApprovalInstanceRevision(left, right) {
|
|
5864
|
-
return (readApprovalInstanceRevision(left) ===
|
|
6423
|
+
return (readApprovalInstanceRevision(left) ===
|
|
6424
|
+
readApprovalInstanceRevision(right) &&
|
|
5865
6425
|
JSON.stringify(left.formData) === JSON.stringify(right.formData));
|
|
5866
6426
|
}
|
|
5867
6427
|
/**
|
|
@@ -5891,7 +6451,9 @@ function readDataSourceFieldKeyAtPath(snapshot, fieldPath) {
|
|
|
5891
6451
|
// A corrupt snapshot can hold non-object entries: read them defensively
|
|
5892
6452
|
// rather than letting a service task throw.
|
|
5893
6453
|
const field = schema.fields.find((candidate) => isRecord(candidate) && candidate.fieldKey === fieldKey);
|
|
5894
|
-
return field && (0, form_1.isFormDataSourceFieldDefinition)(field)
|
|
6454
|
+
return field && (0, form_1.isFormDataSourceFieldDefinition)(field)
|
|
6455
|
+
? field.fieldKey
|
|
6456
|
+
: null;
|
|
5895
6457
|
}
|
|
5896
6458
|
/**
|
|
5897
6459
|
* Refuses form data whose filterable fields hold a value of the wrong type
|
|
@@ -5979,6 +6541,13 @@ function assertServiceTaskFieldValue(snapshot, fieldPath, value, nodeLabel) {
|
|
|
5979
6541
|
function readSnapshotWithoutField(snapshots, fieldKey) {
|
|
5980
6542
|
return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => key !== fieldKey));
|
|
5981
6543
|
}
|
|
6544
|
+
/**
|
|
6545
|
+
* Drops every snapshot that belongs to one of the fields, table cells included
|
|
6546
|
+
* (`items[0].col` belongs to `items`).
|
|
6547
|
+
*/
|
|
6548
|
+
function readSnapshotWithoutFields(snapshots, fieldKeys) {
|
|
6549
|
+
return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => !fieldKeys.has((0, bpm_core_shared_1.readTopLevelFieldKey)(key))));
|
|
6550
|
+
}
|
|
5982
6551
|
/**
|
|
5983
6552
|
* Keeps only the snapshots whose field still carries a value, so clearing a
|
|
5984
6553
|
* dynamic option also removes the label recorded for it.
|
|
@@ -6056,13 +6625,13 @@ function buildInitiatorPolicyContext(initiatorMetadataSnapshot) {
|
|
|
6056
6625
|
subject: initiatorMetadataSnapshot,
|
|
6057
6626
|
};
|
|
6058
6627
|
}
|
|
6059
|
-
function buildWorkflowExpressionContext(instance) {
|
|
6628
|
+
function buildWorkflowExpressionContext(instance, formData) {
|
|
6060
6629
|
return {
|
|
6061
6630
|
env: {
|
|
6062
6631
|
now: new Date().toISOString(),
|
|
6063
6632
|
},
|
|
6064
|
-
form:
|
|
6065
|
-
formData
|
|
6633
|
+
form: formData,
|
|
6634
|
+
formData,
|
|
6066
6635
|
initiator: {
|
|
6067
6636
|
...instance.initiatorMetadataSnapshot,
|
|
6068
6637
|
memberId: instance.initiatorMemberId,
|
|
@@ -6106,8 +6675,8 @@ function buildDryRunExpressionContext(input, lastDecision) {
|
|
|
6106
6675
|
env: {
|
|
6107
6676
|
now: new Date().toISOString(),
|
|
6108
6677
|
},
|
|
6109
|
-
form: input.
|
|
6110
|
-
formData: input.
|
|
6678
|
+
form: input.expressionFormData,
|
|
6679
|
+
formData: input.expressionFormData,
|
|
6111
6680
|
initiator: {
|
|
6112
6681
|
...input.initiatorMetadataSnapshot,
|
|
6113
6682
|
memberId: input.initiatorMemberId,
|
|
@@ -6263,8 +6832,7 @@ function readDefaultReturnTargetNodeId(workflow, currentNode) {
|
|
|
6263
6832
|
}
|
|
6264
6833
|
function isReturnCommentRequired(workflow, nodeId) {
|
|
6265
6834
|
const node = readWorkflowNodeOrThrow(workflow, nodeId);
|
|
6266
|
-
return (node.type === 'userTask' &&
|
|
6267
|
-
node.data.returnBehavior.requireComment === true);
|
|
6835
|
+
return (node.type === 'userTask' && node.data.returnBehavior.requireComment === true);
|
|
6268
6836
|
}
|
|
6269
6837
|
function readReturnResubmitStrategy(task, instance) {
|
|
6270
6838
|
const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
|
|
@@ -6843,9 +7411,7 @@ function readAdhocTargetValue(directive) {
|
|
|
6843
7411
|
webhookHeaders: isRecord(value.webhookHeaders)
|
|
6844
7412
|
? Object.entries(value.webhookHeaders).reduce((accumulator, [key, headerValue]) => ({
|
|
6845
7413
|
...accumulator,
|
|
6846
|
-
...(typeof headerValue === 'string'
|
|
6847
|
-
? { [key]: headerValue }
|
|
6848
|
-
: {}),
|
|
7414
|
+
...(typeof headerValue === 'string' ? { [key]: headerValue } : {}),
|
|
6849
7415
|
}), {})
|
|
6850
7416
|
: undefined,
|
|
6851
7417
|
webhookUrl: typeof value.webhookUrl === 'string' ? value.webhookUrl : '',
|
|
@@ -6961,10 +7527,7 @@ function compareManagerResolution(left, right, orgUnitDepths) {
|
|
|
6961
7527
|
return right.effectiveFrom.localeCompare(left.effectiveFrom);
|
|
6962
7528
|
}
|
|
6963
7529
|
function readOrgUnitDepthMap(orgUnits) {
|
|
6964
|
-
return new Map(orgUnits.map((orgUnit) => [
|
|
6965
|
-
orgUnit.id,
|
|
6966
|
-
orgUnit.path.split('.').length,
|
|
6967
|
-
]));
|
|
7530
|
+
return new Map(orgUnits.map((orgUnit) => [orgUnit.id, orgUnit.path.split('.').length]));
|
|
6968
7531
|
}
|
|
6969
7532
|
function readManagerResolutionScopeRank(scopeType) {
|
|
6970
7533
|
const ranks = {
|
|
@@ -7303,7 +7866,9 @@ function readTableMinRows(field) {
|
|
|
7303
7866
|
return typeof field.minRows === 'number' ? field.minRows : 0;
|
|
7304
7867
|
}
|
|
7305
7868
|
function readTableMaxRows(field) {
|
|
7306
|
-
return typeof field.maxRows === 'number'
|
|
7869
|
+
return typeof field.maxRows === 'number'
|
|
7870
|
+
? field.maxRows
|
|
7871
|
+
: form_1.FORM_TABLE_MAX_ROWS;
|
|
7307
7872
|
}
|
|
7308
7873
|
function readTableRequiredRowCount(field) {
|
|
7309
7874
|
return Math.max(readTableMinRows(field), 1);
|
|
@@ -7663,4 +8228,17 @@ function throwBusinessKeyConflictOrRethrow(error, businessKey) {
|
|
|
7663
8228
|
}
|
|
7664
8229
|
throw error;
|
|
7665
8230
|
}
|
|
8231
|
+
/**
|
|
8232
|
+
* The keys whose value differs between the stored form data and the data a
|
|
8233
|
+
* resubmission replaces it with, sorted. A key present on only one side counts.
|
|
8234
|
+
*/
|
|
8235
|
+
function listChangedFormDataKeys(previous, next) {
|
|
8236
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(next)]);
|
|
8237
|
+
return [...keys]
|
|
8238
|
+
.filter((fieldKey) => Object.prototype.hasOwnProperty.call(previous, fieldKey) !==
|
|
8239
|
+
Object.prototype.hasOwnProperty.call(next, fieldKey) ||
|
|
8240
|
+
(0, form_data_fingerprint_1.computeStableValueFingerprint)(previous[fieldKey]) !==
|
|
8241
|
+
(0, form_data_fingerprint_1.computeStableValueFingerprint)(next[fieldKey]))
|
|
8242
|
+
.sort();
|
|
8243
|
+
}
|
|
7666
8244
|
//# sourceMappingURL=workflow-engine.service.js.map
|