@rytass/bpm-core-nestjs-module 0.21.4 → 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 +87 -0
- package/README.md +9 -1
- 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 +41 -15
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/identity/identity-options.d.ts +1 -1
- package/src/lib/identity/identity.queries.d.ts +0 -2
- package/src/lib/identity/identity.queries.js +0 -11
- package/src/lib/identity/identity.queries.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/initiator-custom-fields-resolver.token.d.ts +31 -11
- package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.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 -5
- package/src/lib/workflow-engine/workflow-engine.queries.js +136 -24
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +216 -7
- package/src/lib/workflow-engine/workflow-engine.service.js +864 -131
- 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,7 +236,10 @@ 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);
|
|
241
|
+
/** In-flight inbox reads by member; see `readSharedOpenInboxTasks`. */
|
|
242
|
+
this.openInboxReadsInFlight = new Map();
|
|
213
243
|
this.approvalInstanceFieldFilterCompiler = new approval_instance_field_filter_compiler_1.ApprovalInstanceFieldFilterCompiler();
|
|
214
244
|
this.approvalInstanceFilterableFieldPolicy = new approval_instance_filterable_field_policy_1.ApprovalInstanceFilterableFieldPolicy();
|
|
215
245
|
/** Webhooks each open engine transaction will send once it has committed. */
|
|
@@ -234,7 +264,68 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
234
264
|
injectedInitiatorCustomFieldsResolver ??
|
|
235
265
|
rootOptions?.initiatorCustomFieldsResolver ??
|
|
236
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;
|
|
237
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;
|
|
238
329
|
}
|
|
239
330
|
/**
|
|
240
331
|
* Today's date as the organization sees it (`organizationTimeZone`), for
|
|
@@ -306,6 +397,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
306
397
|
input.submittedByMemberId.trim() === '')) {
|
|
307
398
|
throw new common_1.BadRequestException('submittedByMemberId must be a non-empty string');
|
|
308
399
|
}
|
|
400
|
+
const hostCustomFields = readHostInitiatorCustomFields(input);
|
|
309
401
|
const submittedFormData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
310
402
|
const businessKey = normalizeBusinessKey(input.businessKey);
|
|
311
403
|
const submittedByMemberId = input.submittedByMemberId ?? input.initiatorMemberId;
|
|
@@ -317,6 +409,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
317
409
|
throw new common_1.ConflictException('Approval template does not have a published version');
|
|
318
410
|
}
|
|
319
411
|
const templateVersion = await this.getPublishedTemplateVersionOrThrow(template.currentVersionId);
|
|
412
|
+
if (input.endUserSubmission === true) {
|
|
413
|
+
assertEndUserMayLaunch(templateVersion);
|
|
414
|
+
}
|
|
320
415
|
const formDefinitionVersion = await this.getPublishedFormVersionOrThrow(templateVersion.formDefinitionVersionId);
|
|
321
416
|
// The fields an approver fills in belong to the approval, not the
|
|
322
417
|
// application: an end user's value for one is dropped (it would otherwise
|
|
@@ -325,13 +420,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
325
420
|
// checked here; required and row minimums wait for the node that fills
|
|
326
421
|
// them. Read off the version the instance is bound to, not the template's
|
|
327
422
|
// latest, so a publish in between cannot change what is stripped.
|
|
328
|
-
const
|
|
423
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(templateVersion.workflowDefinition);
|
|
329
424
|
const formData = input.endUserSubmission === true
|
|
330
|
-
? (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)
|
|
331
426
|
: submittedFormData;
|
|
332
|
-
validateSubmittedFormData(formDefinitionVersion.schema, formData, {
|
|
333
|
-
deferredFieldKeys,
|
|
334
|
-
});
|
|
427
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(formDefinitionVersion.schema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
335
428
|
// Checked against the bound version's schema before any data source is
|
|
336
429
|
// called, so a bad value is refused cheaply. The transaction below
|
|
337
430
|
// confirms under the template lock that this version is still current
|
|
@@ -345,7 +438,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
345
438
|
});
|
|
346
439
|
const initiatorMetadataSnapshot = input.initiatorMetadataSnapshotJson
|
|
347
440
|
? parseJsonObject(input.initiatorMetadataSnapshotJson, 'initiatorMetadataSnapshotJson')
|
|
348
|
-
: await this.buildInitiatorMetadataSnapshot({
|
|
441
|
+
: mergeHostInitiatorCustomFields(await this.buildInitiatorMetadataSnapshot({
|
|
349
442
|
authContext: submitAuthContext,
|
|
350
443
|
memberId: input.initiatorMemberId,
|
|
351
444
|
submission: {
|
|
@@ -353,7 +446,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
353
446
|
templateId: template.id,
|
|
354
447
|
templateVersionId: templateVersion.id,
|
|
355
448
|
},
|
|
356
|
-
});
|
|
449
|
+
}), hostCustomFields);
|
|
357
450
|
if (!this.conditionService.evaluateBoolean(templateVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel',
|
|
358
451
|
// The policy is written in its version's dialect (ADR 22 §3.3).
|
|
359
452
|
readExpressionDialectOptions(templateVersion.initiatorPolicyCel, templateVersion.workflowDefinition))) {
|
|
@@ -394,7 +487,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
394
487
|
const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
|
|
395
488
|
const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
|
|
396
489
|
const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
|
|
397
|
-
const
|
|
490
|
+
const instanceDraft = instanceRepository.create({
|
|
398
491
|
completedAt: null,
|
|
399
492
|
formData,
|
|
400
493
|
formDataOptionSnapshot,
|
|
@@ -414,7 +507,24 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
414
507
|
templateVersionId: templateVersion.id,
|
|
415
508
|
title: input.title?.trim() || template.name,
|
|
416
509
|
workflowSnapshot: templateVersion.workflowDefinition,
|
|
417
|
-
})
|
|
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);
|
|
418
528
|
await this.attachmentService.bindFormDataAttachmentsToInstance(manager, {
|
|
419
529
|
formData,
|
|
420
530
|
instanceId: instance.id,
|
|
@@ -452,6 +562,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
452
562
|
taskId: null,
|
|
453
563
|
}),
|
|
454
564
|
]);
|
|
565
|
+
await this.recordInstanceEvent(manager, instance, instance.state, 'INSTANCE_SUBMITTED', { actorMemberId: submittedByMemberId });
|
|
455
566
|
await this.processRunningInstance(manager, instance);
|
|
456
567
|
return instance;
|
|
457
568
|
}).catch((error) =>
|
|
@@ -492,6 +603,66 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
492
603
|
schema,
|
|
493
604
|
});
|
|
494
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
|
+
}
|
|
495
666
|
async prepareResubmitFormDataOptions(input, submittedFormData, authContext) {
|
|
496
667
|
const instance = await this.approvalInstanceRepository.findOne({
|
|
497
668
|
where: { id: input.instanceId },
|
|
@@ -510,13 +681,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
510
681
|
throw new common_1.ConflictException('Approval template is deactivated');
|
|
511
682
|
}
|
|
512
683
|
const schema = readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot);
|
|
513
|
-
const
|
|
684
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(instance.workflowSnapshot);
|
|
514
685
|
// Same overlay as inside the lock, so the options resolved here are for
|
|
515
686
|
// the form data that is actually saved.
|
|
516
687
|
const formData = input.endUserSubmission === true
|
|
517
|
-
? (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)
|
|
518
689
|
: submittedFormData;
|
|
519
|
-
validateSubmittedFormData(schema, formData, { deferredFieldKeys });
|
|
690
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(schema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
520
691
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData);
|
|
521
692
|
const formDataOptionSnapshot = await this.resolveFormDataOptionSnapshots({
|
|
522
693
|
authContext,
|
|
@@ -554,6 +725,26 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
554
725
|
await this.processRunningInstance(manager, instance);
|
|
555
726
|
});
|
|
556
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
|
+
}
|
|
557
748
|
async decideTask(input, options = {}) {
|
|
558
749
|
return this.runEngineTransaction(async (manager) => {
|
|
559
750
|
const taskRepository = manager.getRepository(task_entity_1.TaskEntity);
|
|
@@ -717,9 +908,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
717
908
|
if (filledFormData !== null && task.isAdhoc) {
|
|
718
909
|
throw new common_1.ForbiddenException(`Ad-hoc task ${task.id} cannot fill in form fields`);
|
|
719
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);
|
|
720
915
|
if (filledFormData !== null) {
|
|
721
916
|
const fillableFieldKeys = (0, bpm_core_shared_1.readNodeFillableFieldKeys)(taskNode);
|
|
722
|
-
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));
|
|
723
919
|
if (refusedFieldKeys.length > 0) {
|
|
724
920
|
throw new common_1.ForbiddenException(`Workflow node ${task.nodeId} does not let approvers fill in: ${refusedFieldKeys.join(', ')}`);
|
|
725
921
|
}
|
|
@@ -730,35 +926,134 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
730
926
|
(0, form_data_fingerprint_1.computeFormDataFingerprint)(instance.formData)) {
|
|
731
927
|
throw new common_1.ConflictException(`Approval instance ${instance.id} form data changed; refresh and retry`);
|
|
732
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);
|
|
733
935
|
// Only keys whose value really changes are written and reported. A key
|
|
734
936
|
// the form data does not have yet counts as changed even when the new
|
|
735
937
|
// value is null.
|
|
736
|
-
const changedFieldKeys =
|
|
938
|
+
const changedFieldKeys = settledFormData === null
|
|
737
939
|
? []
|
|
738
|
-
: Object.keys(
|
|
940
|
+
: Object.keys(settledFormData)
|
|
739
941
|
.filter((fieldKey) => !Object.prototype.hasOwnProperty.call(instance.formData, fieldKey) ||
|
|
740
|
-
(0, form_data_fingerprint_1.computeStableValueFingerprint)(
|
|
942
|
+
(0, form_data_fingerprint_1.computeStableValueFingerprint)(settledFormData[fieldKey]) !==
|
|
741
943
|
(0, form_data_fingerprint_1.computeStableValueFingerprint)(instance.formData[fieldKey]))
|
|
742
944
|
.sort();
|
|
743
|
-
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
|
+
}
|
|
744
952
|
// Every approval at a node that lets its approver fill in fields is
|
|
745
953
|
// held to those fields, whether or not this request sent any: the host
|
|
746
|
-
// 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.
|
|
747
957
|
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED && !task.isAdhoc) {
|
|
748
|
-
const nodeFillableFieldKeys =
|
|
958
|
+
const nodeFillableFieldKeys = new Set([
|
|
959
|
+
...(0, bpm_core_shared_1.readNodeFillableFieldKeys)(taskNode),
|
|
960
|
+
...nodeColumnFillMap.keys(),
|
|
961
|
+
]);
|
|
749
962
|
if (nodeFillableFieldKeys.size > 0) {
|
|
750
|
-
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));
|
|
751
964
|
}
|
|
752
965
|
}
|
|
753
|
-
|
|
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) {
|
|
754
1045
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, mergedFormData, manager);
|
|
1046
|
+
}
|
|
1047
|
+
if (formDataChanged || formDataOptionSnapshotChanged) {
|
|
755
1048
|
// Merged into the shared `instance` before it is signed and before
|
|
756
1049
|
// the token moves on: the signature must cover the filled values, a
|
|
757
1050
|
// gateway after this node must route on them, and an end node saves
|
|
758
|
-
// 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.
|
|
759
1053
|
const savedInstance = await instanceRepository.save({
|
|
760
1054
|
...instance,
|
|
761
1055
|
formData: mergedFormData,
|
|
1056
|
+
formDataOptionSnapshot: nextFormDataOptionSnapshot,
|
|
762
1057
|
});
|
|
763
1058
|
Object.assign(instance, savedInstance);
|
|
764
1059
|
}
|
|
@@ -877,7 +1172,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
877
1172
|
// (rejected / returned, or an approval that completed it), every other
|
|
878
1173
|
// candidate's notification is superseded too; a non-completing approval
|
|
879
1174
|
// on a multi-approver task leaves the remaining approvers' open.
|
|
880
|
-
const taskEnded = input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED ||
|
|
1175
|
+
const taskEnded = input.action !== workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED ||
|
|
1176
|
+
shouldCompleteTask;
|
|
881
1177
|
await this.notificationService.resolveTaskNotifications({
|
|
882
1178
|
actingMemberId: input.decidedByMemberId,
|
|
883
1179
|
manager,
|
|
@@ -928,13 +1224,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
928
1224
|
return decision;
|
|
929
1225
|
}
|
|
930
1226
|
if (input.action === workflow_engine_enums_1.TaskDecisionActionEnum.RETURNED) {
|
|
931
|
-
await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt);
|
|
1227
|
+
await this.returnInstanceToNode(manager, instance, completedTask, returnToNodeId, decidedAt, input.decidedByMemberId);
|
|
932
1228
|
return decision;
|
|
933
1229
|
}
|
|
934
1230
|
if (await this.handleAdhocPreApprovalRejection(manager, instance, completedTask, decidedAt, decisionComment)) {
|
|
935
1231
|
return decision;
|
|
936
1232
|
}
|
|
937
|
-
await this.rejectInstance(manager, instance, completedTask, decidedAt);
|
|
1233
|
+
await this.rejectInstance(manager, instance, completedTask, decidedAt, input.decidedByMemberId);
|
|
938
1234
|
return decision;
|
|
939
1235
|
});
|
|
940
1236
|
}
|
|
@@ -968,6 +1264,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
968
1264
|
},
|
|
969
1265
|
taskId: null,
|
|
970
1266
|
}));
|
|
1267
|
+
await this.recordInstanceEvent(manager, cancelledInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.CANCELLED, 'INSTANCE_CANCELLED', { actorMemberId: input.cancelledByMemberId });
|
|
971
1268
|
return cancelledInstance;
|
|
972
1269
|
});
|
|
973
1270
|
}
|
|
@@ -1005,6 +1302,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1005
1302
|
},
|
|
1006
1303
|
taskId: input.taskId ?? null,
|
|
1007
1304
|
}));
|
|
1305
|
+
await this.recordInstanceEvent(manager, expiredInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.EXPIRED, 'INSTANCE_EXPIRED', {});
|
|
1008
1306
|
return expiredInstance;
|
|
1009
1307
|
});
|
|
1010
1308
|
}
|
|
@@ -1034,6 +1332,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1034
1332
|
}
|
|
1035
1333
|
async resubmitApprovalInstance(input, authContext) {
|
|
1036
1334
|
const submittedFormData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
1335
|
+
if (input.endUserSubmission === true) {
|
|
1336
|
+
await this.assertEndUserMayResubmit(input.instanceId);
|
|
1337
|
+
}
|
|
1037
1338
|
const preparation = this.formDataSourceValueResolver
|
|
1038
1339
|
? await this.prepareResubmitFormDataOptions(input, submittedFormData, authContext ?? createWorkflowAuthContext(input.initiatorMemberId))
|
|
1039
1340
|
: null;
|
|
@@ -1063,20 +1364,37 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1063
1364
|
throw new common_1.ConflictException('Approval instance changed while options were resolving; refresh and retry');
|
|
1064
1365
|
}
|
|
1065
1366
|
const resubmitSchema = readFormDefinitionSnapshotSchema(instance.formDefinitionSnapshot);
|
|
1066
|
-
const
|
|
1367
|
+
const deferral = (0, approver_filled_table_columns_1.readApproverFilledDeferral)(instance.workflowSnapshot);
|
|
1067
1368
|
// An end user gets back the value an approver already filled in, and
|
|
1068
1369
|
// loses a key the stored data never had: the renderer seeds `false`,
|
|
1069
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.
|
|
1070
1372
|
const formData = input.endUserSubmission === true
|
|
1071
|
-
? (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)
|
|
1072
1374
|
: submittedFormData;
|
|
1073
|
-
validateSubmittedFormData(resubmitSchema, formData, {
|
|
1074
|
-
deferredFieldKeys,
|
|
1075
|
-
});
|
|
1375
|
+
validateSubmittedFormData((0, approver_filled_table_columns_1.projectOpenColumnsOptional)(resubmitSchema, deferral), formData, { deferredFieldKeys: deferral.wholeKeys });
|
|
1076
1376
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData, manager);
|
|
1077
1377
|
if (!preparation) {
|
|
1078
1378
|
assertNoUnresolvableDynamicValues(resubmitSchema, formData);
|
|
1079
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
|
+
}
|
|
1080
1398
|
const resubmittedInstance = await instanceRepository.save({
|
|
1081
1399
|
...instance,
|
|
1082
1400
|
completedAt: null,
|
|
@@ -1089,6 +1407,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1089
1407
|
state: workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING,
|
|
1090
1408
|
title: input.title?.trim() || instance.title,
|
|
1091
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 });
|
|
1092
1413
|
await this.attachmentService.bindFormDataAttachmentsToInstance(manager, {
|
|
1093
1414
|
formData,
|
|
1094
1415
|
instanceId: instance.id,
|
|
@@ -1862,11 +2183,13 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1862
2183
|
input.workflowDefinitionJson,
|
|
1863
2184
|
input.formDataJson,
|
|
1864
2185
|
input.initiatorMetadataSnapshotJson ?? '',
|
|
2186
|
+
input.formSchemaJson ?? '',
|
|
1865
2187
|
].some((json) => json.length > MAX_DRY_RUN_INPUT_LENGTH)) {
|
|
1866
2188
|
return createFailedDryRunResult('試跑資料過大,請縮小流程或測試資料後再試');
|
|
1867
2189
|
}
|
|
1868
2190
|
const workflowDefinition = parseWorkflowDefinition(input.workflowDefinitionJson);
|
|
1869
2191
|
const formData = parseJsonObject(input.formDataJson, 'formDataJson');
|
|
2192
|
+
const formSchema = readDryRunFormSchema(input.formSchemaJson);
|
|
1870
2193
|
// The snapshot here is the caller's own sample (the designer always sends
|
|
1871
2194
|
// one) or a bare `{ customFields: {}, memberId }`. It is not the one a real
|
|
1872
2195
|
// submission builds, and a dry run has no template version to hand the
|
|
@@ -1883,6 +2206,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1883
2206
|
return Object.assign(new workflow_dry_run_object_1.WorkflowDryRunResultObject(), {
|
|
1884
2207
|
errors: [],
|
|
1885
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,
|
|
1886
2212
|
formData,
|
|
1887
2213
|
initiatorMemberId: input.initiatorMemberId,
|
|
1888
2214
|
initiatorMetadataSnapshot,
|
|
@@ -2187,6 +2513,51 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2187
2513
|
}
|
|
2188
2514
|
return instance;
|
|
2189
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
|
+
}
|
|
2190
2561
|
/**
|
|
2191
2562
|
* ADR 25 §6: a value written to a filterable field must fit its type, on
|
|
2192
2563
|
* every path that writes form data. The fields checked are the ones the
|
|
@@ -2272,7 +2643,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2272
2643
|
totalPages,
|
|
2273
2644
|
};
|
|
2274
2645
|
}
|
|
2275
|
-
async createFilteredApprovalInstanceQuery(scope,
|
|
2646
|
+
async createFilteredApprovalInstanceQuery(scope, requested = {}) {
|
|
2647
|
+
const options = await this.resolveApprovalInstanceTemplateKey(requested);
|
|
2648
|
+
if (!options) {
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2276
2651
|
const queryBuilder = this.approvalInstanceRepository.createQueryBuilder('approvalInstance');
|
|
2277
2652
|
const hasReadableInstances = this.applyReadableInstanceFilter(queryBuilder, scope, options);
|
|
2278
2653
|
if (!hasReadableInstances) {
|
|
@@ -2501,37 +2876,92 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2501
2876
|
submission: null,
|
|
2502
2877
|
});
|
|
2503
2878
|
return templates.filter((template) => {
|
|
2504
|
-
const
|
|
2879
|
+
const version = template.currentVersionId
|
|
2505
2880
|
? (versionById.get(template.currentVersionId) ?? null)
|
|
2506
2881
|
: null;
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
// so the template is not launchable (fail closed), but only that one:
|
|
2512
|
-
// publishing does not check the field yet (Phase 23 P2), and one such
|
|
2513
|
-
// version must not break the list for every member.
|
|
2514
|
-
if (hasExpressionText(currentVersion.initiatorPolicyCel) &&
|
|
2515
|
-
!isReadableExpressionDialect(currentVersion.workflowDefinition)) {
|
|
2516
|
-
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of launchable templates: unknown expression dialect`);
|
|
2517
|
-
return false;
|
|
2518
|
-
}
|
|
2519
|
-
// Each template is its own operation, so one slow template cannot use
|
|
2520
|
-
// up the allowance of the rest of the list. A policy that throws (for
|
|
2521
|
-
// example a missing `initiator.customFields.*` key, because the resolver
|
|
2522
|
-
// did not return it, or failed and the list fell back to `{}`) is also
|
|
2523
|
-
// only that template's problem: it is left out, and the error code is
|
|
2524
|
-
// logged, never the values.
|
|
2525
|
-
try {
|
|
2526
|
-
return (0, expression_budget_1.runExpressionOperation)(() => this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel', readExpressionDialectOptions(currentVersion.initiatorPolicyCel, currentVersion.workflowDefinition)));
|
|
2527
|
-
}
|
|
2528
|
-
catch (error) {
|
|
2529
|
-
const reason = error instanceof expression_errors_1.ExpressionRuntimeError ? error.code : 'UNEXPECTED';
|
|
2530
|
-
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of launchable templates: initiatorPolicyCel failed (${reason})`);
|
|
2531
|
-
return false;
|
|
2532
|
-
}
|
|
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'));
|
|
2533
2886
|
});
|
|
2534
2887
|
}
|
|
2888
|
+
/**
|
|
2889
|
+
* The published version whose form `memberId` may read data-source options
|
|
2890
|
+
* for when starting `templateId`, or `null` when they may not.
|
|
2891
|
+
*
|
|
2892
|
+
* It applies the same checks as {@link listLaunchableApprovalTemplates} to
|
|
2893
|
+
* that one template, but asks the host's initiator `customFields` resolver
|
|
2894
|
+
* with `purpose: 'FORM_OPTIONS'` and the template and version ids. A host can
|
|
2895
|
+
* therefore open a template's options to members who may not submit it
|
|
2896
|
+
* themselves — a template only the host's backend submits — without the
|
|
2897
|
+
* template appearing in their launchable list. The snapshot is used for this
|
|
2898
|
+
* judgement only and is never stored. A resolver failure judges against
|
|
2899
|
+
* `customFields: {}`, as the list does, so a policy that reads
|
|
2900
|
+
* `initiator.customFields` then fails closed.
|
|
2901
|
+
*/
|
|
2902
|
+
async readFormOptionsTemplateVersion(templateId, memberId, authContext) {
|
|
2903
|
+
const template = await this.approvalTemplateRepository.findOne({
|
|
2904
|
+
where: { id: templateId, isActive: true },
|
|
2905
|
+
});
|
|
2906
|
+
if (!template?.currentVersionId) {
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
const currentVersion = await this.approvalTemplateVersionRepository.findOne({
|
|
2910
|
+
where: {
|
|
2911
|
+
id: template.currentVersionId,
|
|
2912
|
+
status: template_enums_1.ApprovalTemplateVersionStatusEnum.PUBLISHED,
|
|
2913
|
+
},
|
|
2914
|
+
});
|
|
2915
|
+
if (!currentVersion?.formDefinitionVersionId) {
|
|
2916
|
+
return null;
|
|
2917
|
+
}
|
|
2918
|
+
const initiatorMetadataSnapshot = await this.buildInitiatorMetadataSnapshot({
|
|
2919
|
+
authContext: authContext ?? createWorkflowAuthContext(memberId),
|
|
2920
|
+
formOptions: {
|
|
2921
|
+
templateId: template.id,
|
|
2922
|
+
templateVersionId: currentVersion.id,
|
|
2923
|
+
},
|
|
2924
|
+
memberId,
|
|
2925
|
+
submission: null,
|
|
2926
|
+
});
|
|
2927
|
+
return this.isInitiatorPolicyMet(template, currentVersion, initiatorMetadataSnapshot, 'form options')
|
|
2928
|
+
? currentVersion
|
|
2929
|
+
: null;
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Whether `template`'s current published version lets the member described
|
|
2933
|
+
* by `initiatorMetadataSnapshot` start it: the launchable list and the form
|
|
2934
|
+
* options check share this, so the two cannot drift apart. `scope` only
|
|
2935
|
+
* names the caller in the log.
|
|
2936
|
+
*/
|
|
2937
|
+
isInitiatorPolicyMet(template, currentVersion, initiatorMetadataSnapshot, scope) {
|
|
2938
|
+
if (!currentVersion?.formDefinitionVersionId) {
|
|
2939
|
+
return false;
|
|
2940
|
+
}
|
|
2941
|
+
// A policy in a dialect this version does not know cannot be evaluated,
|
|
2942
|
+
// so the template is not launchable (fail closed), but only that one:
|
|
2943
|
+
// publishing does not check the field yet (Phase 23 P2), and one such
|
|
2944
|
+
// version must not break the list for every member.
|
|
2945
|
+
if (hasExpressionText(currentVersion.initiatorPolicyCel) &&
|
|
2946
|
+
!isReadableExpressionDialect(currentVersion.workflowDefinition)) {
|
|
2947
|
+
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of ${scope}: unknown expression dialect`);
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
// Each template is its own operation, so one slow template cannot use
|
|
2951
|
+
// up the allowance of the rest of the list. A policy that throws (for
|
|
2952
|
+
// example a missing `initiator.customFields.*` key, because the resolver
|
|
2953
|
+
// did not return it, or failed and the list fell back to `{}`) is also
|
|
2954
|
+
// only that template's problem: it is left out, and the error code is
|
|
2955
|
+
// logged, never the values.
|
|
2956
|
+
try {
|
|
2957
|
+
return (0, expression_budget_1.runExpressionOperation)(() => this.conditionService.evaluateBoolean(currentVersion.initiatorPolicyCel, buildInitiatorPolicyContext(initiatorMetadataSnapshot), 'initiatorPolicyCel', readExpressionDialectOptions(currentVersion.initiatorPolicyCel, currentVersion.workflowDefinition)));
|
|
2958
|
+
}
|
|
2959
|
+
catch (error) {
|
|
2960
|
+
const reason = error instanceof expression_errors_1.ExpressionRuntimeError ? error.code : 'UNEXPECTED';
|
|
2961
|
+
this.logger.warn(`template=${template.id} version=${currentVersion.id} left out of ${scope}: initiatorPolicyCel failed (${reason})`);
|
|
2962
|
+
return false;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2535
2965
|
async listWorkflowTokens(instanceId, scope) {
|
|
2536
2966
|
await this.getApprovalInstance(instanceId, scope);
|
|
2537
2967
|
return this.workflowTokenRepository.find({
|
|
@@ -2546,8 +2976,112 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2546
2976
|
where: { instanceId },
|
|
2547
2977
|
}));
|
|
2548
2978
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2979
|
+
/**
|
|
2980
|
+
* The member's open tasks, newest first. Without `page` / `pageSize` every
|
|
2981
|
+
* one is returned, as before paging existed. The page is cut from the same
|
|
2982
|
+
* {@link readOpenInboxTasks} result the dashboard counters read, so the list,
|
|
2983
|
+
* {@link countInboxTasks} and the counters cannot disagree; only the page is
|
|
2984
|
+
* given its candidate summaries.
|
|
2985
|
+
*/
|
|
2986
|
+
async listInboxTasks(assigneeMemberId, options = {}) {
|
|
2987
|
+
const openTasks = await this.filterTasksByTemplate(await this.readSharedOpenInboxTasks(assigneeMemberId), options);
|
|
2988
|
+
if (!isTaskListPaged(options)) {
|
|
2989
|
+
return this.attachTaskCandidateSummaries(openTasks);
|
|
2990
|
+
}
|
|
2991
|
+
const offset = readPageOffset(options.page, options.pageSize);
|
|
2992
|
+
return this.attachTaskCandidateSummaries(openTasks.slice(offset, offset + normalizePageSize(options.pageSize)));
|
|
2993
|
+
}
|
|
2994
|
+
/** How many tasks {@link listInboxTasks} pages through. */
|
|
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));
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* {@link readOpenInboxTasks} for the inbox list and its count, joining a
|
|
3069
|
+
* read for the same member that is still in flight. The client asks for
|
|
3070
|
+
* `inboxTasks` and `inboxTaskCount` in one request and GraphQL resolves the
|
|
3071
|
+
* two side by side, so this turns two identical reads into one. An entry
|
|
3072
|
+
* lives only until its read settles: nothing is cached across requests, and
|
|
3073
|
+
* a read that started earlier is never served once it has finished.
|
|
3074
|
+
*/
|
|
3075
|
+
readSharedOpenInboxTasks(assigneeMemberId) {
|
|
3076
|
+
const inFlight = this.openInboxReadsInFlight.get(assigneeMemberId);
|
|
3077
|
+
if (inFlight) {
|
|
3078
|
+
return inFlight;
|
|
3079
|
+
}
|
|
3080
|
+
const read = this.readOpenInboxTasks(assigneeMemberId).finally(() => {
|
|
3081
|
+
this.openInboxReadsInFlight.delete(assigneeMemberId);
|
|
3082
|
+
});
|
|
3083
|
+
this.openInboxReadsInFlight.set(assigneeMemberId, read);
|
|
3084
|
+
return read;
|
|
2551
3085
|
}
|
|
2552
3086
|
/**
|
|
2553
3087
|
* The open tasks that are this member's own to act on — the single
|
|
@@ -2609,33 +3143,104 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2609
3143
|
const settledTaskIds = new Set(settledCandidateRows.map((candidate) => candidate.taskId));
|
|
2610
3144
|
return openTasks.filter((task) => !settledTaskIds.has(task.id));
|
|
2611
3145
|
}
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
3146
|
+
/**
|
|
3147
|
+
* The tasks this member has acted on, most recently acted on first: tasks
|
|
3148
|
+
* assigned to them that are COMPLETED, and tasks on which their own
|
|
3149
|
+
* candidate row is COMPLETED. The latter needs no status on the task: a
|
|
3150
|
+
* countersign still waiting on somebody else already carries their vote, and
|
|
3151
|
+
* the inbox has stopped showing it to them.
|
|
3152
|
+
*
|
|
3153
|
+
* "Acted on" is their candidate row's `decided_at`, else the task's
|
|
3154
|
+
* `completed_at`, else its `created_at`; ties fall back to the task id so no
|
|
3155
|
+
* row moves between pages. Paged in SQL. Without `page` / `pageSize` the
|
|
3156
|
+
* latest {@link DEFAULT_APPROVAL_HISTORY_TASK_LIMIT} are returned, as before
|
|
3157
|
+
* paging existed. `instanceState` keeps only tasks of instances in that
|
|
3158
|
+
* state (RUNNING for "acted on, not finished").
|
|
3159
|
+
*/
|
|
3160
|
+
async listApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
3161
|
+
const paged = isTaskListPaged(options);
|
|
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)
|
|
3167
|
+
.orderBy(this.readApprovalHistoryActedAtExpression(), 'DESC')
|
|
3168
|
+
.addOrderBy('"task"."id"', 'DESC')
|
|
3169
|
+
.offset(paged ? readPageOffset(options.page, options.pageSize) : 0)
|
|
3170
|
+
.limit(paged
|
|
3171
|
+
? normalizePageSize(options.pageSize)
|
|
3172
|
+
: DEFAULT_APPROVAL_HISTORY_TASK_LIMIT)
|
|
3173
|
+
.getMany();
|
|
3174
|
+
return this.attachTaskCandidateSummaries(tasks);
|
|
3175
|
+
}
|
|
3176
|
+
/** How many tasks {@link listApprovalHistoryTasks} pages through. */
|
|
3177
|
+
async countApprovalHistoryTasks(assigneeMemberId, options = {}) {
|
|
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();
|
|
3183
|
+
}
|
|
3184
|
+
/** The rows of {@link listApprovalHistoryTasks}, unordered and unpaged. */
|
|
3185
|
+
createApprovalHistoryTaskQuery(assigneeMemberId, instanceState, templateIds = null) {
|
|
3186
|
+
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
3187
|
+
const queryBuilder = this.taskRepository
|
|
3188
|
+
.createQueryBuilder('task')
|
|
3189
|
+
.andWhere(`
|
|
3190
|
+
(
|
|
3191
|
+
(
|
|
3192
|
+
"task"."assignee_member_id" = :historyMemberId
|
|
3193
|
+
AND "task"."status" = :historyTaskStatus
|
|
3194
|
+
)
|
|
3195
|
+
OR EXISTS (
|
|
3196
|
+
SELECT 1
|
|
3197
|
+
FROM ${candidateTable} "historyCandidate"
|
|
3198
|
+
WHERE "historyCandidate"."task_id" = "task"."id"
|
|
3199
|
+
AND "historyCandidate"."member_id" = :historyMemberId
|
|
3200
|
+
AND "historyCandidate"."status" = :historyCandidateStatus
|
|
3201
|
+
)
|
|
3202
|
+
)
|
|
3203
|
+
`, {
|
|
3204
|
+
historyCandidateStatus: workflow_engine_enums_1.TaskCandidateStatusEnum.COMPLETED,
|
|
3205
|
+
historyMemberId: assigneeMemberId,
|
|
3206
|
+
historyTaskStatus: workflow_engine_enums_1.TaskStatusEnum.COMPLETED,
|
|
2623
3207
|
});
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
3208
|
+
if (instanceState) {
|
|
3209
|
+
queryBuilder.andWhere(`
|
|
3210
|
+
EXISTS (
|
|
3211
|
+
SELECT 1
|
|
3212
|
+
FROM ${this.readQualifiedTableName(this.approvalInstanceRepository)} "historyInstance"
|
|
3213
|
+
WHERE "historyInstance"."id" = "task"."instance_id"
|
|
3214
|
+
AND "historyInstance"."state" = :historyInstanceState
|
|
3215
|
+
)
|
|
3216
|
+
`, { historyInstanceState: instanceState });
|
|
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
|
+
}
|
|
3228
|
+
return queryBuilder;
|
|
3229
|
+
}
|
|
3230
|
+
/** When the member acted on a history task; see {@link listApprovalHistoryTasks}. */
|
|
3231
|
+
readApprovalHistoryActedAtExpression() {
|
|
3232
|
+
const candidateTable = this.readQualifiedTableName(this.taskCandidateRepository);
|
|
3233
|
+
return `COALESCE(
|
|
3234
|
+
(
|
|
3235
|
+
SELECT MAX("actedCandidate"."decided_at")
|
|
3236
|
+
FROM ${candidateTable} "actedCandidate"
|
|
3237
|
+
WHERE "actedCandidate"."task_id" = "task"."id"
|
|
3238
|
+
AND "actedCandidate"."member_id" = :historyMemberId
|
|
3239
|
+
AND "actedCandidate"."status" = :historyCandidateStatus
|
|
3240
|
+
),
|
|
3241
|
+
"task"."completed_at",
|
|
3242
|
+
"task"."created_at"
|
|
3243
|
+
)`;
|
|
2639
3244
|
}
|
|
2640
3245
|
async listTaskDecisions(taskId, scope) {
|
|
2641
3246
|
await this.assertTaskReadable(taskId, scope);
|
|
@@ -2706,7 +3311,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2706
3311
|
* snapshot without a `nodes` array, and a blank or non-string label all
|
|
2707
3312
|
* leave the task without a label, as `readWorkflowNodeLabel` does. Only the
|
|
2708
3313
|
* node's `fieldAccess` leaves the database for the fill flag, which
|
|
2709
|
-
* `
|
|
3314
|
+
* `readNodeFillGrantFieldKeys` then reads as tolerantly as the engine does.
|
|
2710
3315
|
*/
|
|
2711
3316
|
async readTaskNodeSummaries(tasks) {
|
|
2712
3317
|
// The table as the entity reads it, so a configured `schema` is honoured
|
|
@@ -2741,7 +3346,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2741
3346
|
readNodeLabelKey(row.instanceId, row.nodeId),
|
|
2742
3347
|
{
|
|
2743
3348
|
hasFillableFields: row.nodeType === 'userTask' &&
|
|
2744
|
-
(0, bpm_core_shared_1.
|
|
3349
|
+
(0, bpm_core_shared_1.readNodeFillGrantFieldKeys)({
|
|
2745
3350
|
data: { fieldAccess: row.fieldAccess },
|
|
2746
3351
|
}).size > 0,
|
|
2747
3352
|
label: typeof row.label === 'string' && row.label.trim()
|
|
@@ -2793,26 +3398,30 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2793
3398
|
* case on data the host never gave. It runs before the engine
|
|
2794
3399
|
* transaction, so a failure leaves nothing behind and a slow lookup holds
|
|
2795
3400
|
* no lock.
|
|
2796
|
-
* - The list
|
|
2797
|
-
* failing lookup must not take the whole list
|
|
3401
|
+
* - The list, and the form options check, carry on with `customFields: {}`
|
|
3402
|
+
* and log a warning: one failing lookup must not take the whole list
|
|
3403
|
+
* away. That fails closed: a
|
|
2798
3404
|
* policy that reads `initiator.customFields` then throws on the missing key
|
|
2799
3405
|
* and the list's per-template catch leaves that template out, while a
|
|
2800
3406
|
* template whose policy does not read it is listed as usual. Submitting is
|
|
2801
3407
|
* still judged against the resolver's real answer.
|
|
2802
3408
|
*/
|
|
2803
|
-
async buildInitiatorMetadataSnapshot({ authContext, memberId, submission, }) {
|
|
3409
|
+
async buildInitiatorMetadataSnapshot({ authContext, formOptions = null, memberId, submission, }) {
|
|
2804
3410
|
const defaultSnapshot = await this.readDefaultInitiatorMetadataSnapshot(memberId);
|
|
2805
3411
|
if (!this.initiatorCustomFieldsResolver) {
|
|
2806
3412
|
return defaultSnapshot;
|
|
2807
3413
|
}
|
|
3414
|
+
// The template the resolver is asked about: the one being submitted, or
|
|
3415
|
+
// the one whose options are read. The list asks about none.
|
|
3416
|
+
const template = submission ?? formOptions;
|
|
2808
3417
|
const input = {
|
|
2809
3418
|
authContext,
|
|
2810
3419
|
defaultSnapshot: cloneJsonObject(defaultSnapshot),
|
|
2811
3420
|
formData: submission ? cloneJsonObject(submission.formData) : null,
|
|
2812
3421
|
memberId,
|
|
2813
|
-
purpose:
|
|
2814
|
-
templateId:
|
|
2815
|
-
templateVersionId:
|
|
3422
|
+
purpose: readInitiatorCustomFieldsPurpose({ formOptions, submission }),
|
|
3423
|
+
templateId: template?.templateId ?? null,
|
|
3424
|
+
templateVersionId: template?.templateVersionId ?? null,
|
|
2816
3425
|
};
|
|
2817
3426
|
if (submission) {
|
|
2818
3427
|
return {
|
|
@@ -2827,7 +3436,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2827
3436
|
};
|
|
2828
3437
|
}
|
|
2829
3438
|
catch (error) {
|
|
2830
|
-
this.logger.warn(
|
|
3439
|
+
this.logger.warn(`${formOptions
|
|
3440
|
+
? `Form options for template=${formOptions.templateId}`
|
|
3441
|
+
: 'Launchable templates'} judged with customFields {}: the initiator customFields resolver failed (${error instanceof common_1.HttpException ? error.getStatus() : 'UNEXPECTED'})`);
|
|
2831
3442
|
return defaultSnapshot;
|
|
2832
3443
|
}
|
|
2833
3444
|
}
|
|
@@ -2848,9 +3459,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2848
3459
|
* unhandled.
|
|
2849
3460
|
*/
|
|
2850
3461
|
async resolveInitiatorCustomFields(resolver, input) {
|
|
2851
|
-
const subject = input.purpose === '
|
|
2852
|
-
?
|
|
2853
|
-
: '
|
|
3462
|
+
const subject = input.purpose === 'LAUNCHABLE_TEMPLATES'
|
|
3463
|
+
? 'launchable templates'
|
|
3464
|
+
: `${input.purpose === 'FORM_OPTIONS' ? 'form options ' : ''}template=${input.templateId} version=${input.templateVersionId}`;
|
|
2854
3465
|
const outcome = await settleWithin(new Promise((resolve) => {
|
|
2855
3466
|
resolve(resolver.resolveCustomFields(input));
|
|
2856
3467
|
}), INITIATOR_CUSTOM_FIELDS_RESOLVER_TIMEOUT_MS, (late) => {
|
|
@@ -3024,6 +3635,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3024
3635
|
});
|
|
3025
3636
|
await this.dispatchAdhocCompletionNotifications(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED);
|
|
3026
3637
|
await this.cancelPendingAdhocDirectives(manager, instance.id, null);
|
|
3638
|
+
await this.recordInstanceEvent(manager, completedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED, 'INSTANCE_APPROVED', {});
|
|
3027
3639
|
}
|
|
3028
3640
|
async isNodeEntryReady(manager, instance, token, node) {
|
|
3029
3641
|
if (node.type === 'startEvent') {
|
|
@@ -3089,7 +3701,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3089
3701
|
}
|
|
3090
3702
|
isNodeEntryConditionMet(instance, node) {
|
|
3091
3703
|
const entryCondition = readNodeEntryCondition(node);
|
|
3092
|
-
return this.conditionService.evaluateBoolean(entryCondition,
|
|
3704
|
+
return this.conditionService.evaluateBoolean(entryCondition, this.buildExpressionContext(instance), `workflow.nodes.${node.id}.data.entryCondition`, readExpressionDialectOptions(entryCondition, instance.workflowSnapshot));
|
|
3093
3705
|
}
|
|
3094
3706
|
async skipNodeByEntryCondition(manager, instance, token, node) {
|
|
3095
3707
|
await this.skipNode(manager, instance, token, node, {
|
|
@@ -3163,7 +3775,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3163
3775
|
});
|
|
3164
3776
|
}
|
|
3165
3777
|
const context = {
|
|
3166
|
-
...
|
|
3778
|
+
...this.buildExpressionContext(instance),
|
|
3167
3779
|
lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
|
|
3168
3780
|
};
|
|
3169
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) }));
|
|
@@ -3286,6 +3898,12 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3286
3898
|
// worse — an `endState: 'REJECTED'` row overwritten with `APPROVED`
|
|
3287
3899
|
// immediately after it had been committed.
|
|
3288
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
|
+
}
|
|
3289
3907
|
if (instanceState === workflow_engine_enums_1.ApprovalInstanceStateEnum.APPROVED) {
|
|
3290
3908
|
await this.notificationService.createInstanceCompletedNotification({
|
|
3291
3909
|
instance: completedInstance,
|
|
@@ -3387,7 +4005,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3387
4005
|
const singleCandidate = savedCandidates.length === 1 ? savedCandidates[0] : null;
|
|
3388
4006
|
const openCandidates = savedCandidates.filter((candidate) => candidate.status === workflow_engine_enums_1.TaskCandidateStatusEnum.PENDING);
|
|
3389
4007
|
task.openCandidateMemberIds = openCandidates.map((candidate) => candidate.memberId);
|
|
3390
|
-
task.hasFillableFields = (0, bpm_core_shared_1.
|
|
4008
|
+
task.hasFillableFields = (0, bpm_core_shared_1.readNodeFillGrantFieldKeys)(node).size > 0;
|
|
3391
4009
|
await tokenRepository.save({
|
|
3392
4010
|
...token,
|
|
3393
4011
|
status: workflow_engine_enums_1.WorkflowTokenStatusEnum.WAITING,
|
|
@@ -3414,6 +4032,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
3414
4032
|
},
|
|
3415
4033
|
taskId: task.id,
|
|
3416
4034
|
}));
|
|
4035
|
+
await this.recordInstanceEvent(manager, instance, instance.state, 'NODE_ACTIVATED', { nodeId: node.id, taskIds: [task.id] });
|
|
3417
4036
|
await openCandidates.reduce(async (previous, candidate) => {
|
|
3418
4037
|
await previous;
|
|
3419
4038
|
await this.notificationService.createTaskAssignedNotification({
|
|
@@ -4473,9 +5092,22 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4473
5092
|
skippedDelegations: collectSkippedDelegations(delegatedCandidates, true, replacementExcludedMemberIds),
|
|
4474
5093
|
};
|
|
4475
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
|
+
}
|
|
4476
5108
|
buildDelegationContext(instance, nodeId) {
|
|
4477
5109
|
return {
|
|
4478
|
-
formData: instance
|
|
5110
|
+
formData: this.readExpressionFormData(instance),
|
|
4479
5111
|
initiatorMemberId: instance.initiatorMemberId,
|
|
4480
5112
|
initiatorMetadataSnapshot: instance.initiatorMetadataSnapshot,
|
|
4481
5113
|
instanceId: instance.id,
|
|
@@ -4569,7 +5201,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4569
5201
|
return readMemberIdsFromValue(readValueAtPath(instance.formData, resolver.formPath), label, subject, `依表單欄位「${resolver.formPath}」`);
|
|
4570
5202
|
}
|
|
4571
5203
|
if (resolver.type === 'EXPRESSION') {
|
|
4572
|
-
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, '依運算式');
|
|
4573
5205
|
}
|
|
4574
5206
|
if (resolver.type === 'POSITION') {
|
|
4575
5207
|
return this.resolvePositionCandidates(manager, resolver.positionId, label, subject);
|
|
@@ -4699,7 +5331,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4699
5331
|
*/
|
|
4700
5332
|
async readNodeActorApprovals(manager, instance, nodeIds) {
|
|
4701
5333
|
const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
|
|
4702
|
-
where: {
|
|
5334
|
+
where: {
|
|
5335
|
+
instanceId: instance.id,
|
|
5336
|
+
isAdhoc: false,
|
|
5337
|
+
nodeId: (0, typeorm_2.In)([...nodeIds]),
|
|
5338
|
+
},
|
|
4703
5339
|
});
|
|
4704
5340
|
if (tasks.length === 0) {
|
|
4705
5341
|
return [];
|
|
@@ -4715,7 +5351,10 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4715
5351
|
}
|
|
4716
5352
|
const [decisions, decidedLogs] = await Promise.all([
|
|
4717
5353
|
manager.getRepository(task_decision_entity_1.TaskDecisionEntity).find({
|
|
4718
|
-
where: {
|
|
5354
|
+
where: {
|
|
5355
|
+
action: workflow_engine_enums_1.TaskDecisionActionEnum.APPROVED,
|
|
5356
|
+
taskId: (0, typeorm_2.In)(batchTaskIds),
|
|
5357
|
+
},
|
|
4719
5358
|
}),
|
|
4720
5359
|
manager.getRepository(activity_log_entity_1.ActivityLogEntity).find({
|
|
4721
5360
|
where: {
|
|
@@ -5177,11 +5816,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5177
5816
|
const action = node.data.action;
|
|
5178
5817
|
if (action.type === 'WEBHOOK') {
|
|
5179
5818
|
const payload = typeof action.payload === 'string' && action.payload.trim()
|
|
5180
|
-
? this.conditionService.evaluateValue(action.payload,
|
|
5819
|
+
? this.conditionService.evaluateValue(action.payload, this.buildExpressionContext(instance), `workflow.nodes.${node.id}.data.action.payload`, {
|
|
5181
5820
|
dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot),
|
|
5182
5821
|
})
|
|
5183
5822
|
: {
|
|
5184
|
-
instance:
|
|
5823
|
+
instance: this.buildExpressionContext(instance).instance,
|
|
5185
5824
|
triggeredAt: new Date().toISOString(),
|
|
5186
5825
|
};
|
|
5187
5826
|
await this.dispatchWebhookAfterCommit(manager, {
|
|
@@ -5209,7 +5848,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5209
5848
|
return;
|
|
5210
5849
|
}
|
|
5211
5850
|
if (action.type === 'SET_FORM_FIELD') {
|
|
5212
|
-
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) });
|
|
5213
5852
|
assertServiceTaskFieldValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
|
|
5214
5853
|
const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
|
|
5215
5854
|
await this.assertCurrentFilterableFormDataValues(instance.templateId, updatedFormData, manager, node.data.label);
|
|
@@ -5271,8 +5910,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5271
5910
|
// §3.5). Without the webhook module there is nothing to queue into; the
|
|
5272
5911
|
// publish lint already refuses a template that references an endpoint
|
|
5273
5912
|
// in that case.
|
|
5274
|
-
const webhookDeliveryIds = this.webhookDeliveryService &&
|
|
5275
|
-
(0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
|
|
5913
|
+
const webhookDeliveryIds = this.webhookDeliveryService && (0, workflow_graph_1.readNotifyWebhookTargets)(action).length > 0
|
|
5276
5914
|
? await this.webhookDeliveryService.enqueueNotifyWebhooks(manager, {
|
|
5277
5915
|
instance: {
|
|
5278
5916
|
formData: instance.formData,
|
|
@@ -5302,7 +5940,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5302
5940
|
taskId: null,
|
|
5303
5941
|
}));
|
|
5304
5942
|
}
|
|
5305
|
-
async rejectInstance(manager, instance, task, rejectedAt) {
|
|
5943
|
+
async rejectInstance(manager, instance, task, rejectedAt, decidedByMemberId) {
|
|
5306
5944
|
const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
|
|
5307
5945
|
// Dispatch pending ad-hoc notifications for the rejecting node before the
|
|
5308
5946
|
// remaining directives are cancelled by the terminal cleanup below.
|
|
@@ -5333,8 +5971,9 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5333
5971
|
},
|
|
5334
5972
|
taskId: task.id,
|
|
5335
5973
|
}));
|
|
5974
|
+
await this.recordInstanceEvent(manager, rejectedInstance, workflow_engine_enums_1.ApprovalInstanceStateEnum.REJECTED, 'INSTANCE_REJECTED', { actorMemberId: decidedByMemberId });
|
|
5336
5975
|
}
|
|
5337
|
-
async returnInstanceToNode(manager, instance, task, returnToNodeId, returnedAt) {
|
|
5976
|
+
async returnInstanceToNode(manager, instance, task, returnToNodeId, returnedAt, decidedByMemberId) {
|
|
5338
5977
|
if (!returnToNodeId) {
|
|
5339
5978
|
throw new common_1.ConflictException('Return target node is required');
|
|
5340
5979
|
}
|
|
@@ -5392,6 +6031,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
5392
6031
|
taskId: null,
|
|
5393
6032
|
}),
|
|
5394
6033
|
]);
|
|
6034
|
+
await this.recordInstanceEvent(manager, returnedInstance, returnedInstance.state, 'INSTANCE_RETURNED', { actorMemberId: decidedByMemberId, nodeId: task.nodeId });
|
|
5395
6035
|
if (returnedInstance.state === workflow_engine_enums_1.ApprovalInstanceStateEnum.RUNNING) {
|
|
5396
6036
|
await this.processRunningInstance(manager, returnedInstance);
|
|
5397
6037
|
}
|
|
@@ -5593,6 +6233,9 @@ exports.WorkflowEngineService = WorkflowEngineService = WorkflowEngineService_1
|
|
|
5593
6233
|
tslib_1.__param(20, (0, common_1.Optional)()),
|
|
5594
6234
|
tslib_1.__param(21, (0, common_1.Optional)()),
|
|
5595
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)()),
|
|
5596
6239
|
tslib_1.__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
5597
6240
|
typeorm_2.Repository,
|
|
5598
6241
|
typeorm_2.Repository,
|
|
@@ -5609,7 +6252,7 @@ exports.WorkflowEngineService = WorkflowEngineService = WorkflowEngineService_1
|
|
|
5609
6252
|
delegation_service_1.DelegationService,
|
|
5610
6253
|
notification_service_1.NotificationService,
|
|
5611
6254
|
signature_service_1.SignatureService,
|
|
5612
|
-
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])
|
|
5613
6256
|
], WorkflowEngineService);
|
|
5614
6257
|
function parseJsonObject(value, label) {
|
|
5615
6258
|
try {
|
|
@@ -5632,9 +6275,77 @@ function readDefaultInitiatorMetadataSnapshot(memberId) {
|
|
|
5632
6275
|
memberId,
|
|
5633
6276
|
};
|
|
5634
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
|
+
}
|
|
5635
6339
|
function cloneJsonObject(value) {
|
|
5636
6340
|
return JSON.parse(JSON.stringify(value));
|
|
5637
6341
|
}
|
|
6342
|
+
/** Which caller is building an initiator snapshot, as the resolver is told. */
|
|
6343
|
+
function readInitiatorCustomFieldsPurpose({ formOptions, submission, }) {
|
|
6344
|
+
if (submission) {
|
|
6345
|
+
return 'SUBMISSION';
|
|
6346
|
+
}
|
|
6347
|
+
return formOptions ? 'FORM_OPTIONS' : 'LAUNCHABLE_TEMPLATES';
|
|
6348
|
+
}
|
|
5638
6349
|
/**
|
|
5639
6350
|
* What a host's initiator `customFields` resolver returned, as the object the
|
|
5640
6351
|
* snapshot keeps: a plain object that survives JSON, which is also how it is
|
|
@@ -5709,7 +6420,8 @@ function createWorkflowAuthContext(memberId) {
|
|
|
5709
6420
|
};
|
|
5710
6421
|
}
|
|
5711
6422
|
function isSameApprovalInstanceRevision(left, right) {
|
|
5712
|
-
return (readApprovalInstanceRevision(left) ===
|
|
6423
|
+
return (readApprovalInstanceRevision(left) ===
|
|
6424
|
+
readApprovalInstanceRevision(right) &&
|
|
5713
6425
|
JSON.stringify(left.formData) === JSON.stringify(right.formData));
|
|
5714
6426
|
}
|
|
5715
6427
|
/**
|
|
@@ -5739,7 +6451,9 @@ function readDataSourceFieldKeyAtPath(snapshot, fieldPath) {
|
|
|
5739
6451
|
// A corrupt snapshot can hold non-object entries: read them defensively
|
|
5740
6452
|
// rather than letting a service task throw.
|
|
5741
6453
|
const field = schema.fields.find((candidate) => isRecord(candidate) && candidate.fieldKey === fieldKey);
|
|
5742
|
-
return field && (0, form_1.isFormDataSourceFieldDefinition)(field)
|
|
6454
|
+
return field && (0, form_1.isFormDataSourceFieldDefinition)(field)
|
|
6455
|
+
? field.fieldKey
|
|
6456
|
+
: null;
|
|
5743
6457
|
}
|
|
5744
6458
|
/**
|
|
5745
6459
|
* Refuses form data whose filterable fields hold a value of the wrong type
|
|
@@ -5827,6 +6541,13 @@ function assertServiceTaskFieldValue(snapshot, fieldPath, value, nodeLabel) {
|
|
|
5827
6541
|
function readSnapshotWithoutField(snapshots, fieldKey) {
|
|
5828
6542
|
return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => key !== fieldKey));
|
|
5829
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
|
+
}
|
|
5830
6551
|
/**
|
|
5831
6552
|
* Keeps only the snapshots whose field still carries a value, so clearing a
|
|
5832
6553
|
* dynamic option also removes the label recorded for it.
|
|
@@ -5904,13 +6625,13 @@ function buildInitiatorPolicyContext(initiatorMetadataSnapshot) {
|
|
|
5904
6625
|
subject: initiatorMetadataSnapshot,
|
|
5905
6626
|
};
|
|
5906
6627
|
}
|
|
5907
|
-
function buildWorkflowExpressionContext(instance) {
|
|
6628
|
+
function buildWorkflowExpressionContext(instance, formData) {
|
|
5908
6629
|
return {
|
|
5909
6630
|
env: {
|
|
5910
6631
|
now: new Date().toISOString(),
|
|
5911
6632
|
},
|
|
5912
|
-
form:
|
|
5913
|
-
formData
|
|
6633
|
+
form: formData,
|
|
6634
|
+
formData,
|
|
5914
6635
|
initiator: {
|
|
5915
6636
|
...instance.initiatorMetadataSnapshot,
|
|
5916
6637
|
memberId: instance.initiatorMemberId,
|
|
@@ -5954,8 +6675,8 @@ function buildDryRunExpressionContext(input, lastDecision) {
|
|
|
5954
6675
|
env: {
|
|
5955
6676
|
now: new Date().toISOString(),
|
|
5956
6677
|
},
|
|
5957
|
-
form: input.
|
|
5958
|
-
formData: input.
|
|
6678
|
+
form: input.expressionFormData,
|
|
6679
|
+
formData: input.expressionFormData,
|
|
5959
6680
|
initiator: {
|
|
5960
6681
|
...input.initiatorMetadataSnapshot,
|
|
5961
6682
|
memberId: input.initiatorMemberId,
|
|
@@ -6111,8 +6832,7 @@ function readDefaultReturnTargetNodeId(workflow, currentNode) {
|
|
|
6111
6832
|
}
|
|
6112
6833
|
function isReturnCommentRequired(workflow, nodeId) {
|
|
6113
6834
|
const node = readWorkflowNodeOrThrow(workflow, nodeId);
|
|
6114
|
-
return (node.type === 'userTask' &&
|
|
6115
|
-
node.data.returnBehavior.requireComment === true);
|
|
6835
|
+
return (node.type === 'userTask' && node.data.returnBehavior.requireComment === true);
|
|
6116
6836
|
}
|
|
6117
6837
|
function readReturnResubmitStrategy(task, instance) {
|
|
6118
6838
|
const node = readWorkflowNodeOrThrow(instance.workflowSnapshot, task.nodeId);
|
|
@@ -6376,6 +7096,9 @@ function readPageOffset(page, pageSize) {
|
|
|
6376
7096
|
const normalizedPageSize = normalizePageSize(pageSize);
|
|
6377
7097
|
return (normalizePage(page) - 1) * normalizedPageSize;
|
|
6378
7098
|
}
|
|
7099
|
+
function isTaskListPaged(options) {
|
|
7100
|
+
return options.page !== undefined || options.pageSize !== undefined;
|
|
7101
|
+
}
|
|
6379
7102
|
function shouldPaginateList(options) {
|
|
6380
7103
|
return (typeof options.page !== 'undefined' ||
|
|
6381
7104
|
typeof options.pageSize !== 'undefined');
|
|
@@ -6688,9 +7411,7 @@ function readAdhocTargetValue(directive) {
|
|
|
6688
7411
|
webhookHeaders: isRecord(value.webhookHeaders)
|
|
6689
7412
|
? Object.entries(value.webhookHeaders).reduce((accumulator, [key, headerValue]) => ({
|
|
6690
7413
|
...accumulator,
|
|
6691
|
-
...(typeof headerValue === 'string'
|
|
6692
|
-
? { [key]: headerValue }
|
|
6693
|
-
: {}),
|
|
7414
|
+
...(typeof headerValue === 'string' ? { [key]: headerValue } : {}),
|
|
6694
7415
|
}), {})
|
|
6695
7416
|
: undefined,
|
|
6696
7417
|
webhookUrl: typeof value.webhookUrl === 'string' ? value.webhookUrl : '',
|
|
@@ -6806,10 +7527,7 @@ function compareManagerResolution(left, right, orgUnitDepths) {
|
|
|
6806
7527
|
return right.effectiveFrom.localeCompare(left.effectiveFrom);
|
|
6807
7528
|
}
|
|
6808
7529
|
function readOrgUnitDepthMap(orgUnits) {
|
|
6809
|
-
return new Map(orgUnits.map((orgUnit) => [
|
|
6810
|
-
orgUnit.id,
|
|
6811
|
-
orgUnit.path.split('.').length,
|
|
6812
|
-
]));
|
|
7530
|
+
return new Map(orgUnits.map((orgUnit) => [orgUnit.id, orgUnit.path.split('.').length]));
|
|
6813
7531
|
}
|
|
6814
7532
|
function readManagerResolutionScopeRank(scopeType) {
|
|
6815
7533
|
const ranks = {
|
|
@@ -7148,7 +7866,9 @@ function readTableMinRows(field) {
|
|
|
7148
7866
|
return typeof field.minRows === 'number' ? field.minRows : 0;
|
|
7149
7867
|
}
|
|
7150
7868
|
function readTableMaxRows(field) {
|
|
7151
|
-
return typeof field.maxRows === 'number'
|
|
7869
|
+
return typeof field.maxRows === 'number'
|
|
7870
|
+
? field.maxRows
|
|
7871
|
+
: form_1.FORM_TABLE_MAX_ROWS;
|
|
7152
7872
|
}
|
|
7153
7873
|
function readTableRequiredRowCount(field) {
|
|
7154
7874
|
return Math.max(readTableMinRows(field), 1);
|
|
@@ -7508,4 +8228,17 @@ function throwBusinessKeyConflictOrRethrow(error, businessKey) {
|
|
|
7508
8228
|
}
|
|
7509
8229
|
throw error;
|
|
7510
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
|
+
}
|
|
7511
8244
|
//# sourceMappingURL=workflow-engine.service.js.map
|