@rytass/bpm-core-nestjs-module 0.20.0 → 0.21.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 +60 -0
- package/README.md +4 -4
- package/package.json +2 -2
- package/src/lib/condition/expression-errors.js +17 -1
- package/src/lib/condition/expression-errors.js.map +1 -1
- package/src/lib/form/form-data-issue.d.ts +34 -0
- package/src/lib/form/form-data-issue.js +59 -0
- package/src/lib/form/form-data-issue.js.map +1 -0
- package/src/lib/form/form-member-value.validator.d.ts +2 -2
- package/src/lib/form/form-member-value.validator.js +2 -3
- package/src/lib/form/form-member-value.validator.js.map +1 -1
- package/src/lib/form/form-org-unit-value.validator.d.ts +2 -2
- package/src/lib/form/form-org-unit-value.validator.js +4 -1
- package/src/lib/form/form-org-unit-value.validator.js.map +1 -1
- package/src/lib/form/form-schema.validator.js +40 -0
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.d.ts +29 -0
- package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.js +44 -0
- package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.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/template/filterable-field-guard.d.ts +49 -0
- package/src/lib/template/filterable-field-guard.js +141 -0
- package/src/lib/template/filterable-field-guard.js.map +1 -0
- package/src/lib/template/filterable-field-issue.object.d.ts +21 -0
- package/src/lib/template/filterable-field-issue.object.js +65 -0
- package/src/lib/template/filterable-field-issue.object.js.map +1 -0
- package/src/lib/template/filterable-field.queries.d.ts +20 -0
- package/src/lib/template/filterable-field.queries.js +80 -0
- package/src/lib/template/filterable-field.queries.js.map +1 -0
- package/src/lib/template/index.d.ts +1 -0
- package/src/lib/template/index.js +1 -0
- package/src/lib/template/index.js.map +1 -1
- package/src/lib/template/template.module.js +2 -0
- package/src/lib/template/template.module.js.map +1 -1
- package/src/lib/template/template.service.d.ts +39 -0
- package/src/lib/template/template.service.js +167 -44
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/workflow-engine/approval-instance-field-filter.compiler.d.ts +49 -0
- package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js +501 -0
- package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js.map +1 -0
- package/src/lib/workflow-engine/approval-instance-filterable-field.policy.d.ts +15 -0
- package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js +20 -0
- package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js.map +1 -0
- package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.d.ts +15 -0
- package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js +55 -0
- package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js.map +1 -0
- package/src/lib/workflow-engine/dto/approval-instance-list-args.d.ts +9 -0
- package/src/lib/workflow-engine/dto/approval-instance-list-args.js +24 -0
- package/src/lib/workflow-engine/dto/approval-instance-list-args.js.map +1 -0
- package/src/lib/workflow-engine/dto/approval-instance-sort.input.d.ts +10 -0
- package/src/lib/workflow-engine/dto/approval-instance-sort.input.js +32 -0
- package/src/lib/workflow-engine/dto/approval-instance-sort.input.js.map +1 -0
- package/src/lib/workflow-engine/workflow-condition-evaluator.js +12 -12
- package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.enums.d.ts +12 -0
- package/src/lib/workflow-engine/workflow-engine.enums.js +21 -1
- package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.queries.d.ts +5 -3
- package/src/lib/workflow-engine/workflow-engine.queries.js +31 -9
- package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +35 -4
- package/src/lib/workflow-engine/workflow-engine.service.js +342 -81
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-form-value.d.ts +11 -0
- package/src/lib/workflow-engine/workflow-form-value.js +33 -0
- package/src/lib/workflow-engine/workflow-form-value.js.map +1 -0
|
@@ -18,6 +18,7 @@ const form_definition_version_entity_1 = require("../form/form-definition-versio
|
|
|
18
18
|
const form_enums_1 = require("../form/form.enums");
|
|
19
19
|
const form_data_source_errors_1 = require("../form-data-source/form-data-source.errors");
|
|
20
20
|
const form_member_value_validator_1 = require("../form/form-member-value.validator");
|
|
21
|
+
const form_data_issue_1 = require("../form/form-data-issue");
|
|
21
22
|
const form_org_unit_value_validator_1 = require("../form/form-org-unit-value.validator");
|
|
22
23
|
const form_data_source_types_1 = require("../form-data-source/form-data-source.types");
|
|
23
24
|
const sla_schedule_service_1 = require("../calendar/sla-schedule.service");
|
|
@@ -39,6 +40,8 @@ const activity_log_entity_1 = require("./activity-log.entity");
|
|
|
39
40
|
const adhoc_directive_entity_1 = require("./adhoc-directive.entity");
|
|
40
41
|
const adhoc_enums_1 = require("./adhoc.enums");
|
|
41
42
|
const approval_instance_entity_1 = require("./approval-instance.entity");
|
|
43
|
+
const approval_instance_field_filter_compiler_1 = require("./approval-instance-field-filter.compiler");
|
|
44
|
+
const approval_instance_filterable_field_policy_1 = require("./approval-instance-filterable-field.policy");
|
|
42
45
|
const text_field_pattern_test_1 = require("./text-field-pattern-test");
|
|
43
46
|
const form_data_fingerprint_1 = require("./form-data-fingerprint");
|
|
44
47
|
const instance_submitter_1 = require("./instance-submitter");
|
|
@@ -47,6 +50,7 @@ const task_candidate_entity_1 = require("./task-candidate.entity");
|
|
|
47
50
|
const task_entity_1 = require("./task.entity");
|
|
48
51
|
const workflow_engine_enums_1 = require("./workflow-engine.enums");
|
|
49
52
|
const workflow_condition_evaluator_1 = require("./workflow-condition-evaluator");
|
|
53
|
+
const workflow_form_value_1 = require("./workflow-form-value");
|
|
50
54
|
const workflow_dry_run_object_1 = require("./workflow-dry-run.object");
|
|
51
55
|
const time_budget_1 = require("./time-budget");
|
|
52
56
|
const expression_budget_1 = require("../condition/expression-budget");
|
|
@@ -194,6 +198,8 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
194
198
|
this.formDataSourceValueResolver = formDataSourceValueResolver;
|
|
195
199
|
this.webhookDeliveryService = webhookDeliveryService;
|
|
196
200
|
this.logger = new common_1.Logger(WorkflowEngineService_1.name);
|
|
201
|
+
this.approvalInstanceFieldFilterCompiler = new approval_instance_field_filter_compiler_1.ApprovalInstanceFieldFilterCompiler();
|
|
202
|
+
this.approvalInstanceFilterableFieldPolicy = new approval_instance_filterable_field_policy_1.ApprovalInstanceFilterableFieldPolicy();
|
|
197
203
|
/** Webhooks each open engine transaction will send once it has committed. */
|
|
198
204
|
this.deferredWebhookDispatches = new WeakMap();
|
|
199
205
|
// A binding under the token wins: that is either the host's own provider
|
|
@@ -307,6 +313,11 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
307
313
|
validateSubmittedFormData(formDefinitionVersion.schema, formData, {
|
|
308
314
|
deferredFieldKeys,
|
|
309
315
|
});
|
|
316
|
+
// Checked against the bound version's schema before any data source is
|
|
317
|
+
// called, so a bad value is refused cheaply. The transaction below
|
|
318
|
+
// confirms under the template lock that this version is still current
|
|
319
|
+
// (ADR 25 §15), which is what makes this schema the one in force.
|
|
320
|
+
assertFilterableFormDataValues(formDefinitionVersion.schema, formData);
|
|
310
321
|
const formDataOptionSnapshot = await this.resolveFormDataOptionSnapshots({
|
|
311
322
|
authContext: authContext ?? createWorkflowAuthContext(input.initiatorMemberId),
|
|
312
323
|
formData,
|
|
@@ -333,6 +344,26 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
333
344
|
throw new common_1.BadRequestException('Published workflow does not include a startEvent');
|
|
334
345
|
}
|
|
335
346
|
return this.runEngineTransaction(async (manager) => {
|
|
347
|
+
// ADR 25 §6 and §15: take FOR SHARE on the template row, so a publish,
|
|
348
|
+
// deactivation or edit cannot commit between this point and the
|
|
349
|
+
// insert, then re-check under the lock what was read before it. The
|
|
350
|
+
// filterable check above used the version read then; it holds only
|
|
351
|
+
// while that version is still the current one.
|
|
352
|
+
const lockedTemplate = await manager
|
|
353
|
+
.getRepository(approval_template_entity_1.ApprovalTemplateEntity)
|
|
354
|
+
.findOne({
|
|
355
|
+
lock: { mode: 'pessimistic_read' },
|
|
356
|
+
where: { deletedAt: (0, typeorm_2.IsNull)(), id: template.id },
|
|
357
|
+
});
|
|
358
|
+
if (!lockedTemplate) {
|
|
359
|
+
throw new common_1.NotFoundException(`Approval template ${template.id} was not found`);
|
|
360
|
+
}
|
|
361
|
+
if (!lockedTemplate.isActive) {
|
|
362
|
+
throw new common_1.ConflictException('Approval template is deactivated');
|
|
363
|
+
}
|
|
364
|
+
if (lockedTemplate.currentVersionId !== templateVersion.id) {
|
|
365
|
+
throw new common_1.ConflictException('範本在送出期間發布了新版本,這次申請沒有送出;請重新整理頁面後再送出');
|
|
366
|
+
}
|
|
336
367
|
const instanceRepository = manager.getRepository(approval_instance_entity_1.ApprovalInstanceEntity);
|
|
337
368
|
const tokenRepository = manager.getRepository(workflow_token_entity_1.WorkflowTokenEntity);
|
|
338
369
|
const activityRepository = manager.getRepository(activity_log_entity_1.ActivityLogEntity);
|
|
@@ -459,6 +490,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
459
490
|
? (0, bpm_core_shared_1.overlayStoredFormDataKeys)(submittedFormData, instance.formData, deferredFieldKeys)
|
|
460
491
|
: submittedFormData;
|
|
461
492
|
validateSubmittedFormData(schema, formData, { deferredFieldKeys });
|
|
493
|
+
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData);
|
|
462
494
|
const formDataOptionSnapshot = await this.resolveFormDataOptionSnapshots({
|
|
463
495
|
authContext,
|
|
464
496
|
formData,
|
|
@@ -672,6 +704,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
672
704
|
}
|
|
673
705
|
}
|
|
674
706
|
if (filledFormData !== null && changedFieldKeys.length > 0) {
|
|
707
|
+
await this.assertCurrentFilterableFormDataValues(instance.templateId, mergedFormData, manager);
|
|
675
708
|
// Merged into the shared `instance` before it is signed and before
|
|
676
709
|
// the token moves on: the signature must cover the filled values, a
|
|
677
710
|
// gateway after this node must route on them, and an end node saves
|
|
@@ -993,6 +1026,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
993
1026
|
validateSubmittedFormData(resubmitSchema, formData, {
|
|
994
1027
|
deferredFieldKeys,
|
|
995
1028
|
});
|
|
1029
|
+
await this.assertCurrentFilterableFormDataValues(instance.templateId, formData, manager);
|
|
996
1030
|
if (!preparation) {
|
|
997
1031
|
assertNoUnresolvableDynamicValues(resubmitSchema, formData);
|
|
998
1032
|
}
|
|
@@ -1950,14 +1984,14 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
1950
1984
|
};
|
|
1951
1985
|
}
|
|
1952
1986
|
evaluateDryRunEdge(input, edge, lastDecision) {
|
|
1953
|
-
const expression = edge.
|
|
1954
|
-
if (expression
|
|
1955
|
-
return
|
|
1956
|
-
allowedRootIdentifiers: DRY_RUN_EDGE_ROOT_IDENTIFIERS,
|
|
1957
|
-
dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
|
|
1958
|
-
});
|
|
1987
|
+
const expression = readEvaluableEdgeExpression(edge, input.formData);
|
|
1988
|
+
if (!expression) {
|
|
1989
|
+
return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData: input.formData });
|
|
1959
1990
|
}
|
|
1960
|
-
return (
|
|
1991
|
+
return evaluateWithStructuredFallback(edge, input.formData, (message) => this.logger.warn(`dry-run ${message}`), () => this.conditionService.evaluateBoolean(expression, buildDryRunExpressionContext(input, lastDecision), `workflow.edges.${edge.id}.data.condition`, {
|
|
1992
|
+
allowedRootIdentifiers: DRY_RUN_EDGE_ROOT_IDENTIFIERS,
|
|
1993
|
+
dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(input.workflowDefinition),
|
|
1994
|
+
}));
|
|
1961
1995
|
}
|
|
1962
1996
|
/**
|
|
1963
1997
|
* The dry-run assignee of a user task. Only resolvers whose dry run yields
|
|
@@ -2084,22 +2118,76 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2084
2118
|
}
|
|
2085
2119
|
return instance;
|
|
2086
2120
|
}
|
|
2121
|
+
/**
|
|
2122
|
+
* ADR 25 §6: a value written to a filterable field must fit its type, on
|
|
2123
|
+
* every path that writes form data. The fields checked are the ones the
|
|
2124
|
+
* template's *current* version marks filterable, not the instance's own
|
|
2125
|
+
* snapshot: an older instance can be re-filled, resubmitted or written by a
|
|
2126
|
+
* service task after a newer version enabled filtering on a field, and the
|
|
2127
|
+
* enable check (ADR 25 §5.4) guarantees the type is the same in every version
|
|
2128
|
+
* that holds instances, so the current definition judges the old value too.
|
|
2129
|
+
*/
|
|
2130
|
+
async assertCurrentFilterableFormDataValues(templateId, formData, manager, serviceNodeLabel) {
|
|
2131
|
+
const schema = await this.readCurrentFormSchema(templateId, manager);
|
|
2132
|
+
assertFilterableFormDataValues(schema, formData, serviceNodeLabel);
|
|
2133
|
+
}
|
|
2134
|
+
/** The form schema of the template's current version, or `null` when there is none. */
|
|
2135
|
+
async readCurrentFormSchema(templateId, manager) {
|
|
2136
|
+
// Inside a write transaction the template row is taken FOR SHARE (ADR 25
|
|
2137
|
+
// §15): a publish, compose or rollback needs FOR UPDATE on the same row, so
|
|
2138
|
+
// the current version read here cannot change before the write commits.
|
|
2139
|
+
const template = manager
|
|
2140
|
+
? await manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity).findOne({
|
|
2141
|
+
lock: { mode: 'pessimistic_read' },
|
|
2142
|
+
where: { id: templateId },
|
|
2143
|
+
})
|
|
2144
|
+
: await this.approvalTemplateRepository.findOne({
|
|
2145
|
+
where: { id: templateId },
|
|
2146
|
+
});
|
|
2147
|
+
if (!template?.currentVersionId) {
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
const versions = manager
|
|
2151
|
+
? manager.getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity)
|
|
2152
|
+
: this.approvalTemplateVersionRepository;
|
|
2153
|
+
const version = await versions.findOne({
|
|
2154
|
+
where: { id: template.currentVersionId },
|
|
2155
|
+
});
|
|
2156
|
+
if (!version?.formDefinitionVersionId) {
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
const formVersions = manager
|
|
2160
|
+
? manager.getRepository(form_definition_version_entity_1.FormDefinitionVersionEntity)
|
|
2161
|
+
: this.formDefinitionVersionRepository;
|
|
2162
|
+
const formVersion = await formVersions.findOne({
|
|
2163
|
+
where: { id: version.formDefinitionVersionId },
|
|
2164
|
+
});
|
|
2165
|
+
return formVersion?.schema ?? null;
|
|
2166
|
+
}
|
|
2087
2167
|
async listApprovalInstances(scope, options = {}) {
|
|
2088
|
-
const
|
|
2089
|
-
if (!
|
|
2168
|
+
const query = await this.createFilteredApprovalInstanceQuery(scope, options);
|
|
2169
|
+
if (!query) {
|
|
2090
2170
|
return [];
|
|
2091
2171
|
}
|
|
2092
|
-
|
|
2172
|
+
const { orderBy, queryBuilder } = query;
|
|
2173
|
+
orderBy.forEach((order, index) => {
|
|
2174
|
+
if (index === 0) {
|
|
2175
|
+
queryBuilder.orderBy(order.sql, order.direction);
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
queryBuilder.addOrderBy(order.sql, order.direction);
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2093
2181
|
if (shouldPaginateList(options)) {
|
|
2094
2182
|
queryBuilder
|
|
2095
|
-
.
|
|
2096
|
-
.
|
|
2183
|
+
.offset(readPageOffset(options.page, options.pageSize))
|
|
2184
|
+
.limit(normalizePageSize(options.pageSize));
|
|
2097
2185
|
}
|
|
2098
2186
|
return queryBuilder.getMany();
|
|
2099
2187
|
}
|
|
2100
2188
|
async countApprovalInstances(scope, options = {}) {
|
|
2101
|
-
const
|
|
2102
|
-
return
|
|
2189
|
+
const query = await this.createFilteredApprovalInstanceQuery(scope, options);
|
|
2190
|
+
return query ? query.queryBuilder.getCount() : 0;
|
|
2103
2191
|
}
|
|
2104
2192
|
async readApprovalInstancePageInfo(scope, options = {}) {
|
|
2105
2193
|
const totalCount = await this.countApprovalInstances(scope, options);
|
|
@@ -2115,7 +2203,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2115
2203
|
totalPages,
|
|
2116
2204
|
};
|
|
2117
2205
|
}
|
|
2118
|
-
async
|
|
2206
|
+
async createFilteredApprovalInstanceQuery(scope, options = {}) {
|
|
2119
2207
|
const queryBuilder = this.approvalInstanceRepository.createQueryBuilder('approvalInstance');
|
|
2120
2208
|
const hasReadableInstances = this.applyReadableInstanceFilter(queryBuilder, scope, options);
|
|
2121
2209
|
if (!hasReadableInstances) {
|
|
@@ -2133,7 +2221,72 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2133
2221
|
});
|
|
2134
2222
|
}
|
|
2135
2223
|
applyApprovalInstanceSearchFilter(queryBuilder, options.searchText);
|
|
2136
|
-
|
|
2224
|
+
const compiled = await this.compileApprovalInstanceFieldQuery(scope, templateId, options);
|
|
2225
|
+
compiled.conditions.forEach((condition) => {
|
|
2226
|
+
queryBuilder.andWhere(condition.sql, condition.parameters);
|
|
2227
|
+
});
|
|
2228
|
+
queryBuilder.setParameters(compiled.orderBy.reduce((all, order) => ({ ...all, ...order.parameters }), {}));
|
|
2229
|
+
return { orderBy: compiled.orderBy, queryBuilder };
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* The WHERE conditions and sort keys of the field filters and sort on the
|
|
2233
|
+
* approval-instance list (ADR 25 §7). The template's current schema is read,
|
|
2234
|
+
* and the static option lists of every version holding instances, only when
|
|
2235
|
+
* a condition or a non-time sort key needs them.
|
|
2236
|
+
*/
|
|
2237
|
+
async compileApprovalInstanceFieldQuery(scope, templateId, options) {
|
|
2238
|
+
const fieldFilters = options.fieldFilters ?? [];
|
|
2239
|
+
const sort = options.sort ?? [];
|
|
2240
|
+
const needsSchema = fieldFilters.length > 0 ||
|
|
2241
|
+
sort.some((item) => !(0, bpm_core_shared_1.isApprovalInstanceSortTimeField)(item.field));
|
|
2242
|
+
if (needsSchema && !templateId) {
|
|
2243
|
+
throw new common_1.BadRequestException('篩選或排序表單欄位時需要指定簽核範本');
|
|
2244
|
+
}
|
|
2245
|
+
const schema = needsSchema && templateId
|
|
2246
|
+
? await this.readCurrentFormSchema(templateId)
|
|
2247
|
+
: null;
|
|
2248
|
+
// Only a name search (CONTAINS) reads option names; other conditions and
|
|
2249
|
+
// sort keys need the current schema alone.
|
|
2250
|
+
const needsOptionNames = fieldFilters.some((filter) => filter.operator === 'CONTAINS');
|
|
2251
|
+
const staticOptionsByPath = needsOptionNames && templateId
|
|
2252
|
+
? await this.readStaticOptionsByPathForTemplate(templateId, schema)
|
|
2253
|
+
: (0, approval_instance_field_filter_compiler_1.readStaticOptionsByPath)([]);
|
|
2254
|
+
return this.approvalInstanceFieldFilterCompiler.compile({
|
|
2255
|
+
alias: 'approvalInstance',
|
|
2256
|
+
fieldFilters,
|
|
2257
|
+
paths: this.approvalInstanceFilterableFieldPolicy.readFilterableFieldPaths(scope, schema),
|
|
2258
|
+
sort,
|
|
2259
|
+
staticOptionsByPath,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* Static option lists by field path, across the current schema and the form
|
|
2264
|
+
* version of every template version that holds instances (ADR 25 §7.3): a
|
|
2265
|
+
* CONTAINS on a retired option still matches its name.
|
|
2266
|
+
*/
|
|
2267
|
+
async readStaticOptionsByPathForTemplate(templateId, currentSchema) {
|
|
2268
|
+
const held = await this.approvalInstanceRepository
|
|
2269
|
+
.createQueryBuilder('approvalInstance')
|
|
2270
|
+
.select('DISTINCT "approvalInstance"."template_version_id"', 'id')
|
|
2271
|
+
.where('"approvalInstance"."template_id" = :templateId', { templateId })
|
|
2272
|
+
.getRawMany();
|
|
2273
|
+
const templateVersions = held.length === 0
|
|
2274
|
+
? []
|
|
2275
|
+
: await this.approvalTemplateVersionRepository.find({
|
|
2276
|
+
where: { id: (0, typeorm_2.In)(held.map((row) => row.id)) },
|
|
2277
|
+
});
|
|
2278
|
+
const formVersionIds = templateVersions
|
|
2279
|
+
.map((version) => version.formDefinitionVersionId)
|
|
2280
|
+
.filter((id) => id !== null);
|
|
2281
|
+
const formVersions = formVersionIds.length === 0
|
|
2282
|
+
? []
|
|
2283
|
+
: await this.formDefinitionVersionRepository.find({
|
|
2284
|
+
where: { id: (0, typeorm_2.In)(formVersionIds) },
|
|
2285
|
+
});
|
|
2286
|
+
return (0, approval_instance_field_filter_compiler_1.readStaticOptionsByPath)([
|
|
2287
|
+
currentSchema,
|
|
2288
|
+
...formVersions.map((version) => version.schema),
|
|
2289
|
+
]);
|
|
2137
2290
|
}
|
|
2138
2291
|
applyReadableInstanceFilter(queryBuilder, scope, options) {
|
|
2139
2292
|
if (!scope) {
|
|
@@ -2802,15 +2955,17 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
2802
2955
|
throw new common_1.ConflictException(`Workflow node ${node.id} has no matching outgoing edge`);
|
|
2803
2956
|
}
|
|
2804
2957
|
async evaluateWorkflowEdge(manager, instance, node, edge) {
|
|
2805
|
-
const expression = edge.
|
|
2806
|
-
if (expression
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
|
|
2811
|
-
}, `workflow.edges.${edge.id}.data.condition`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) });
|
|
2958
|
+
const expression = readEvaluableEdgeExpression(edge, instance.formData);
|
|
2959
|
+
if (!expression) {
|
|
2960
|
+
return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, {
|
|
2961
|
+
formData: instance.formData,
|
|
2962
|
+
});
|
|
2812
2963
|
}
|
|
2813
|
-
|
|
2964
|
+
const context = {
|
|
2965
|
+
...buildWorkflowExpressionContext(instance),
|
|
2966
|
+
lastDecision: await this.readLastDecisionContext(manager, instance.id, node.id),
|
|
2967
|
+
};
|
|
2968
|
+
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) }));
|
|
2814
2969
|
}
|
|
2815
2970
|
async readLastDecisionContext(manager, instanceId, nodeId) {
|
|
2816
2971
|
const tasks = await manager.getRepository(task_entity_1.TaskEntity).find({
|
|
@@ -4646,6 +4801,7 @@ let WorkflowEngineService = WorkflowEngineService_1 = class WorkflowEngineServic
|
|
|
4646
4801
|
const value = this.conditionService.evaluateValue(action.value, buildWorkflowExpressionContext(instance), `workflow.nodes.${node.id}.data.action.value`, { dialect: (0, expression_dialect_1.readWorkflowExpressionDialect)(instance.workflowSnapshot) });
|
|
4647
4802
|
assertServiceTaskTextValue(instance.formDefinitionSnapshot, action.fieldPath, value, node.data.label);
|
|
4648
4803
|
const updatedFormData = writeValueAtPath(instance.formData, action.fieldPath, value);
|
|
4804
|
+
await this.assertCurrentFilterableFormDataValues(instance.templateId, updatedFormData, manager, node.data.label);
|
|
4649
4805
|
// A service task runs inside this transaction, so it cannot call a host
|
|
4650
4806
|
// provider to re-resolve the label. Rather than leave the previous
|
|
4651
4807
|
// option snapshot describing a value that no longer exists, drop it: the
|
|
@@ -5104,6 +5260,52 @@ function readDataSourceFieldKeyAtPath(snapshot, fieldPath) {
|
|
|
5104
5260
|
const field = schema.fields.find((candidate) => isRecord(candidate) && candidate.fieldKey === fieldKey);
|
|
5105
5261
|
return field && (0, form_1.isFormDataSourceFieldDefinition)(field) ? field.fieldKey : null;
|
|
5106
5262
|
}
|
|
5263
|
+
/**
|
|
5264
|
+
* Refuses form data whose filterable fields hold a value of the wrong type
|
|
5265
|
+
* (ADR 25 §6). The list query reads these values with type guards, so a wrong
|
|
5266
|
+
* value would silently fall out of every filter; refusing the write is what
|
|
5267
|
+
* keeps filter results trustworthy.
|
|
5268
|
+
*/
|
|
5269
|
+
function assertFilterableFormDataValues(schema, formData, serviceNodeLabel) {
|
|
5270
|
+
const problems = (0, bpm_core_shared_1.readFilterableValueProblems)((0, bpm_core_shared_1.readFilterableFieldPaths)(schema), formData);
|
|
5271
|
+
if (problems.length === 0) {
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
const issues = problems.map((problem) => (0, form_data_issue_1.createFormDataIssue)(readFilterableProblemLocation(schema, problem), 'TYPE', `必須是${problem.expected}`));
|
|
5275
|
+
const details = (0, form_data_issue_1.readFormDataIssueMessage)(issues);
|
|
5276
|
+
throw (0, form_data_issue_1.createFormDataBadRequest)(serviceNodeLabel
|
|
5277
|
+
? `系統節點「${serviceNodeLabel}」要寫入的表單值不符合可篩選欄位的型別(${details}),這次操作沒有生效;請聯絡流程管理者修正此節點的寫入值`
|
|
5278
|
+
: `可篩選欄位的值型別不正確:${details}`, issues);
|
|
5279
|
+
}
|
|
5280
|
+
/** Where a filterable problem sits: a field, a table row, or one cell of a row. */
|
|
5281
|
+
function readFilterableProblemLocation(schema, problem) {
|
|
5282
|
+
const [tableKey, columnKey] = problem.path.split('.');
|
|
5283
|
+
const table = schema?.fields.find((field) => field.fieldKey === tableKey && (0, form_1.isTableFieldDefinition)(field));
|
|
5284
|
+
const column = table && (0, form_1.isTableFieldDefinition)(table) && columnKey !== undefined
|
|
5285
|
+
? table.columns.find((candidate) => candidate.fieldKey === columnKey)
|
|
5286
|
+
: undefined;
|
|
5287
|
+
if (table && (0, form_1.isTableFieldDefinition)(table)) {
|
|
5288
|
+
if (problem.rowIndex === null) {
|
|
5289
|
+
return (0, form_data_issue_1.readFieldIssueLocation)(table);
|
|
5290
|
+
}
|
|
5291
|
+
const rowLocation = (0, form_data_issue_1.readRowIssueLocation)(table, problem.rowIndex);
|
|
5292
|
+
return column ? (0, form_data_issue_1.readCellIssueLocation)(rowLocation, column) : rowLocation;
|
|
5293
|
+
}
|
|
5294
|
+
const field = schema?.fields.find((candidate) => candidate.fieldKey === problem.path);
|
|
5295
|
+
if (field) {
|
|
5296
|
+
return (0, form_data_issue_1.readFieldIssueLocation)(field);
|
|
5297
|
+
}
|
|
5298
|
+
// Not in the schema (the check and this lookup read the same schema, so
|
|
5299
|
+
// this is defensive): still address it the way FormDataIssue.path promises,
|
|
5300
|
+
// `table[i].col` for a row value rather than the dotted filter path.
|
|
5301
|
+
const row = `${tableKey}[${problem.rowIndex}]`;
|
|
5302
|
+
const path = problem.rowIndex === null
|
|
5303
|
+
? problem.path
|
|
5304
|
+
: columnKey === undefined
|
|
5305
|
+
? row
|
|
5306
|
+
: `${row}.${columnKey}`;
|
|
5307
|
+
return { path, ref: `欄位「${path}」` };
|
|
5308
|
+
}
|
|
5107
5309
|
/**
|
|
5108
5310
|
* Holds a `SET_FORM_FIELD` write to the length and format rules the form
|
|
5109
5311
|
* applies to a filler, so a service task cannot store text the field would
|
|
@@ -5129,8 +5331,8 @@ function assertServiceTaskTextValue(snapshot, fieldPath, value, nodeLabel) {
|
|
|
5129
5331
|
if (!field || errors.length === 0) {
|
|
5130
5332
|
return;
|
|
5131
5333
|
}
|
|
5132
|
-
throw new common_1.BadRequestException(`系統節點「${nodeLabel}
|
|
5133
|
-
|
|
5334
|
+
throw new common_1.BadRequestException(`系統節點「${nodeLabel}」要寫入的值不符合欄位規則(${(0, form_data_issue_1.readFormDataIssueMessage)(errors)}),` +
|
|
5335
|
+
`這次操作沒有生效;請聯絡流程管理者修正此節點的寫入值`);
|
|
5134
5336
|
}
|
|
5135
5337
|
function readSnapshotWithoutField(snapshots, fieldKey) {
|
|
5136
5338
|
return Object.fromEntries(Object.entries(snapshots ?? {}).filter(([key]) => key !== fieldKey));
|
|
@@ -5445,22 +5647,67 @@ function readNodeEntryCondition(node) {
|
|
|
5445
5647
|
return null;
|
|
5446
5648
|
}
|
|
5447
5649
|
/**
|
|
5448
|
-
*
|
|
5449
|
-
*
|
|
5450
|
-
*
|
|
5451
|
-
*
|
|
5452
|
-
*
|
|
5453
|
-
* edges never routed as designed. The structured fields the designer stores
|
|
5454
|
-
* beside the CEL are evaluated instead: `evaluateWorkflowEdgeCondition`
|
|
5455
|
-
* already reads a list as contains / does not contain and an empty list as
|
|
5456
|
-
* empty, the meaning the current designer compiles too. A hand-written CEL
|
|
5457
|
-
* carries no structured fields and is never overridden.
|
|
5650
|
+
* The stored CEL an edge is evaluated by, or `null` when its structured fields
|
|
5651
|
+
* answer directly: no CEL, or a designer-built edge whose value is a list
|
|
5652
|
+
* (`evaluateWorkflowEdgeCondition` reads a list as contains / does not contain
|
|
5653
|
+
* and an empty list as empty, which the stored CEL of older definitions did
|
|
5654
|
+
* not).
|
|
5458
5655
|
*/
|
|
5459
|
-
function
|
|
5656
|
+
function readEvaluableEdgeExpression(edge, formData) {
|
|
5657
|
+
const expression = edge.data.condition?.trim();
|
|
5460
5658
|
const fieldKey = edge.data.conditionFieldKey;
|
|
5461
|
-
|
|
5659
|
+
const listValued = Boolean(fieldKey &&
|
|
5462
5660
|
edge.data.conditionOperator &&
|
|
5463
|
-
Array.isArray(formData
|
|
5661
|
+
Array.isArray((0, workflow_form_value_1.readOwnFormValue)(formData, fieldKey)));
|
|
5662
|
+
return expression && !listValued ? expression : null;
|
|
5663
|
+
}
|
|
5664
|
+
/**
|
|
5665
|
+
* Evaluates an edge's stored CEL and, only when it fails because the edge's
|
|
5666
|
+
* own field is unfilled, answers from the structured fields instead. The one
|
|
5667
|
+
* place both the actual run and the designer dry run decide that, so the two
|
|
5668
|
+
* cannot drift apart.
|
|
5669
|
+
*
|
|
5670
|
+
* Published definitions and in-flight instances keep the CEL they were
|
|
5671
|
+
* stored with, and that CEL throws for an optional field left untouched: the
|
|
5672
|
+
* key is absent (`Identifier "agent" not found`), or an ordered comparison
|
|
5673
|
+
* meets `null` or `""` (`null > 5`, `"" > 5`). Only then does the structured
|
|
5674
|
+
* evaluator answer, reading the value as unfilled (`EQUALS` and the ordered
|
|
5675
|
+
* comparisons false, `NOT_EQUALS` and `IS_EMPTY` true, `IS_FILLED` false).
|
|
5676
|
+
*
|
|
5677
|
+
* - `EXPRESSION_MISSING_KEY` falls back only when the missing key *is* the
|
|
5678
|
+
* edge's field. A CEL edited by hand to also read another key
|
|
5679
|
+
* (`form.agent != null && initiator.department == "X"`) fails as written
|
|
5680
|
+
* when that other key is missing, even if the edge's field is unfilled.
|
|
5681
|
+
* - `EXPRESSION_TYPE_ERROR` carries no key, so it falls back whenever the
|
|
5682
|
+
* edge's field is unfilled, and each such fallback is logged (`warn`, no
|
|
5683
|
+
* values) so an unrelated type error answered this way stays traceable.
|
|
5684
|
+
* - Everything else is rethrown: a stored CEL that evaluates keeps its
|
|
5685
|
+
* result, so no route that worked before changes; an error while the
|
|
5686
|
+
* edge's field is filled points at the CEL itself; a hand-written CEL has
|
|
5687
|
+
* no structured fields; a budget overrun or a function failure is never
|
|
5688
|
+
* absorbed.
|
|
5689
|
+
*/
|
|
5690
|
+
function evaluateWithStructuredFallback(edge, formData, warn, evaluate) {
|
|
5691
|
+
try {
|
|
5692
|
+
return evaluate();
|
|
5693
|
+
}
|
|
5694
|
+
catch (error) {
|
|
5695
|
+
const fieldKey = edge.data.conditionFieldKey;
|
|
5696
|
+
if (!fieldKey ||
|
|
5697
|
+
!edge.data.conditionOperator ||
|
|
5698
|
+
!(error instanceof expression_errors_1.ExpressionRuntimeError) ||
|
|
5699
|
+
!(0, workflow_form_value_1.isEmptyWorkflowValue)((0, workflow_form_value_1.readOwnFormValue)(formData, fieldKey))) {
|
|
5700
|
+
throw error;
|
|
5701
|
+
}
|
|
5702
|
+
if (error.code === 'EXPRESSION_MISSING_KEY' && error.keyName === fieldKey) {
|
|
5703
|
+
return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData });
|
|
5704
|
+
}
|
|
5705
|
+
if (error.code === 'EXPRESSION_TYPE_ERROR') {
|
|
5706
|
+
warn(`code=EDGE_STRUCTURED_FALLBACK edge=${edge.id} field=${fieldKey} reason=EXPRESSION_TYPE_ERROR`);
|
|
5707
|
+
return (0, workflow_condition_evaluator_1.evaluateWorkflowEdgeCondition)(edge, { formData });
|
|
5708
|
+
}
|
|
5709
|
+
throw error;
|
|
5710
|
+
}
|
|
5464
5711
|
}
|
|
5465
5712
|
function edgeHasCondition(edge) {
|
|
5466
5713
|
return Boolean(edge.data.condition?.trim() ||
|
|
@@ -6160,29 +6407,28 @@ function validateFilledFormFields(schema, formData, fieldKeys, requiredFieldKeys
|
|
|
6160
6407
|
* field wins over a text value breaking its length or format rule, which wins
|
|
6161
6408
|
* over a malformed member value, which wins over a malformed org unit value,
|
|
6162
6409
|
* which wins over a table problem, and the later checks are only evaluated
|
|
6163
|
-
* when the earlier ones pass.
|
|
6410
|
+
* when the earlier ones pass. The refusal carries every issue of the winning
|
|
6411
|
+
* check. Its message is those issues' sentences joined, except for missing
|
|
6412
|
+
* required fields, which keep the one-line summary `缺少必填欄位:A, B` that
|
|
6413
|
+
* submitters read; each issue there still says `欄位「A」為必填`. A client
|
|
6414
|
+
* that places errors on fields reads the issues, not the message.
|
|
6164
6415
|
*/
|
|
6165
6416
|
function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readOrgUnitErrors, readTableErrors, readTextErrors, }) {
|
|
6166
6417
|
if (missingFields.length > 0) {
|
|
6167
|
-
throw
|
|
6418
|
+
throw (0, form_data_issue_1.createFormDataBadRequest)(`缺少必填欄位:${missingFields
|
|
6168
6419
|
.map((field) => field.label || field.fieldKey)
|
|
6169
|
-
.join(', ')}
|
|
6170
|
-
}
|
|
6171
|
-
const textErrors = readTextErrors();
|
|
6172
|
-
if (textErrors.length > 0) {
|
|
6173
|
-
throw new common_1.BadRequestException(textErrors.join('; '));
|
|
6174
|
-
}
|
|
6175
|
-
const memberErrors = readMemberErrors();
|
|
6176
|
-
if (memberErrors.length > 0) {
|
|
6177
|
-
throw new common_1.BadRequestException(memberErrors.join('; '));
|
|
6420
|
+
.join(', ')}`, missingFields.map((field) => (0, form_data_issue_1.createFormDataIssue)((0, form_data_issue_1.readFieldIssueLocation)(field), 'REQUIRED', '為必填')));
|
|
6178
6421
|
}
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6422
|
+
// Each check runs only when every earlier one passed, so a later check's
|
|
6423
|
+
// work is never done for a request that is already refused.
|
|
6424
|
+
const winning = [
|
|
6425
|
+
readTextErrors,
|
|
6426
|
+
readMemberErrors,
|
|
6427
|
+
readOrgUnitErrors,
|
|
6428
|
+
readTableErrors,
|
|
6429
|
+
].reduce((found, check) => (found.length > 0 ? found : check()), []);
|
|
6430
|
+
if (winning.length > 0) {
|
|
6431
|
+
throw (0, form_data_issue_1.createFormDataBadRequest)((0, form_data_issue_1.readFormDataIssueMessage)(winning), winning);
|
|
6186
6432
|
}
|
|
6187
6433
|
}
|
|
6188
6434
|
/**
|
|
@@ -6191,64 +6437,73 @@ function throwFirstFormDataFieldError({ missingFields, readMemberErrors, readOrg
|
|
|
6191
6437
|
* the flat text fields share. `minimum` / `maximum` stay a frontend hint.
|
|
6192
6438
|
*/
|
|
6193
6439
|
function validateSubmittedTableValue(field, value, required, testPattern, shapeOnly = false) {
|
|
6194
|
-
const
|
|
6440
|
+
const location = (0, form_data_issue_1.readFieldIssueLocation)(field);
|
|
6195
6441
|
if (typeof value === 'undefined' || value === null) {
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6442
|
+
if (shapeOnly) {
|
|
6443
|
+
return [];
|
|
6444
|
+
}
|
|
6445
|
+
if (required || readTableMinRows(field) > 0) {
|
|
6446
|
+
const rows = readTableRequiredRowCount(field);
|
|
6447
|
+
return [
|
|
6448
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'MIN_ROWS', `至少需要 ${rows} 列`, rows),
|
|
6449
|
+
];
|
|
6450
|
+
}
|
|
6451
|
+
return [];
|
|
6201
6452
|
}
|
|
6202
6453
|
if (!Array.isArray(value)) {
|
|
6203
|
-
return [
|
|
6454
|
+
return [(0, form_data_issue_1.createFormDataIssue)(location, 'TYPE', '必須是表格列的清單')];
|
|
6204
6455
|
}
|
|
6205
6456
|
// Too many rows is refused before any row is checked: the rows a submitter
|
|
6206
6457
|
// sends would otherwise decide how many cells the text patterns run on.
|
|
6207
6458
|
const maxRows = readTableMaxRows(field);
|
|
6208
6459
|
if (value.length > maxRows) {
|
|
6209
|
-
return [
|
|
6460
|
+
return [
|
|
6461
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'MAX_ROWS', `最多 ${maxRows} 列`, maxRows),
|
|
6462
|
+
];
|
|
6210
6463
|
}
|
|
6211
|
-
const rowErrors = value.flatMap((row, index) => validateSubmittedTableRow(field, row,
|
|
6464
|
+
const rowErrors = value.flatMap((row, index) => validateSubmittedTableRow(field, row, (0, form_data_issue_1.readRowIssueLocation)(field, index), testPattern, shapeOnly));
|
|
6212
6465
|
// A malformed row cannot be counted against the minimum meaningfully; report
|
|
6213
6466
|
// the shape problem first so the submitter fixes the real cause.
|
|
6214
6467
|
if (rowErrors.length > 0) {
|
|
6215
6468
|
return rowErrors;
|
|
6216
6469
|
}
|
|
6217
|
-
return validateSubmittedTableRowCount(field, value.length, required,
|
|
6470
|
+
return validateSubmittedTableRowCount(field, value.length, required, location, shapeOnly);
|
|
6218
6471
|
}
|
|
6219
6472
|
/** The minimum only: the maximum is refused before any row is checked. */
|
|
6220
|
-
function validateSubmittedTableRowCount(field, rowCount, required,
|
|
6473
|
+
function validateSubmittedTableRowCount(field, rowCount, required, location, shapeOnly = false) {
|
|
6221
6474
|
const minRows = shapeOnly
|
|
6222
6475
|
? 0
|
|
6223
6476
|
: required
|
|
6224
6477
|
? Math.max(readTableMinRows(field), 1)
|
|
6225
6478
|
: readTableMinRows(field);
|
|
6226
6479
|
return rowCount < minRows
|
|
6227
|
-
? [
|
|
6480
|
+
? [
|
|
6481
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'MIN_ROWS', `至少需要 ${minRows} 列`, minRows),
|
|
6482
|
+
]
|
|
6228
6483
|
: [];
|
|
6229
6484
|
}
|
|
6230
|
-
function validateSubmittedTableRow(field, row,
|
|
6485
|
+
function validateSubmittedTableRow(field, row, location, testPattern, shapeOnly = false) {
|
|
6231
6486
|
if (!isRecord(row)) {
|
|
6232
|
-
return [
|
|
6487
|
+
return [(0, form_data_issue_1.createFormDataIssue)(location, 'TYPE', '必須是物件')];
|
|
6233
6488
|
}
|
|
6234
6489
|
const columnsByKey = new Map(field.columns.map((column) => [column.fieldKey, column]));
|
|
6235
6490
|
const unknownKeyErrors = Object.keys(row)
|
|
6236
6491
|
.filter((columnKey) => !columnsByKey.has(columnKey))
|
|
6237
|
-
.map((columnKey) =>
|
|
6492
|
+
.map((columnKey) => (0, form_data_issue_1.createFormDataIssue)(location, 'UNKNOWN_COLUMN', `含有不屬於此表格的欄位「${columnKey}」`));
|
|
6238
6493
|
const cellErrors = Object.entries(row)
|
|
6239
6494
|
.filter(([columnKey, cellValue]) => columnsByKey.has(columnKey) && !(0, form_1.isFormTableCellValue)(cellValue))
|
|
6240
|
-
.map(([columnKey]) =>
|
|
6495
|
+
.map(([columnKey]) => (0, form_data_issue_1.createFormDataIssue)((0, form_data_issue_1.readCellIssueLocation)(location, columnsByKey.get(columnKey) ?? { fieldKey: columnKey }), 'TYPE', '不是有效的值'));
|
|
6241
6496
|
const requiredErrors = shapeOnly
|
|
6242
6497
|
? []
|
|
6243
6498
|
: field.columns
|
|
6244
6499
|
.filter((column) => isSubmittedTableColumnRequired(column) &&
|
|
6245
6500
|
!isSubmittedFieldValuePresent(readRowCellValue(row, column.fieldKey)))
|
|
6246
|
-
.map((column) =>
|
|
6501
|
+
.map((column) => (0, form_data_issue_1.createFormDataIssue)((0, form_data_issue_1.readCellIssueLocation)(location, column), 'REQUIRED', '為必填'));
|
|
6247
6502
|
// A shape-only pass covers a table nobody fills in, so the filler-facing
|
|
6248
6503
|
// rules do not apply to it, the same as the required check above; only a
|
|
6249
6504
|
// cell of the wrong type is still refused.
|
|
6250
6505
|
const textErrors = field.columns.flatMap((column) => readSubmittedTextValueErrors(column, readRowCellValue(row, column.fieldKey), {
|
|
6251
|
-
|
|
6506
|
+
location: (0, form_data_issue_1.readCellIssueLocation)(location, column),
|
|
6252
6507
|
shapeOnly,
|
|
6253
6508
|
testPattern,
|
|
6254
6509
|
}));
|
|
@@ -6265,7 +6520,7 @@ function validateSubmittedTableRow(field, row, path, testPattern, shapeOnly = fa
|
|
|
6265
6520
|
* budget is spent, a value is reported as not checked rather than as a
|
|
6266
6521
|
* format error.
|
|
6267
6522
|
*/
|
|
6268
|
-
function readSubmittedTextValueErrors(field, value, {
|
|
6523
|
+
function readSubmittedTextValueErrors(field, value, { location = (0, form_data_issue_1.readFieldIssueLocation)(field), shapeOnly = false, testPattern, }) {
|
|
6269
6524
|
if (field.type !== 'text' && field.type !== 'textarea') {
|
|
6270
6525
|
return [];
|
|
6271
6526
|
}
|
|
@@ -6284,21 +6539,27 @@ function readSubmittedTextValueErrors(field, value, { path = `formData.${field.f
|
|
|
6284
6539
|
};
|
|
6285
6540
|
const issue = readIssue();
|
|
6286
6541
|
if (issue === 'timeout') {
|
|
6287
|
-
return [
|
|
6542
|
+
return [
|
|
6543
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'PATTERN_TIMEOUT', '無法在時限內完成格式檢查'),
|
|
6544
|
+
];
|
|
6288
6545
|
}
|
|
6289
6546
|
if (!issue || (shapeOnly && issue.kind !== 'type')) {
|
|
6290
6547
|
return [];
|
|
6291
6548
|
}
|
|
6292
6549
|
if (issue.kind === 'type') {
|
|
6293
|
-
return [
|
|
6550
|
+
return [(0, form_data_issue_1.createFormDataIssue)(location, 'TYPE', '必須是文字')];
|
|
6294
6551
|
}
|
|
6295
6552
|
if (issue.kind === 'minLength') {
|
|
6296
|
-
return [
|
|
6553
|
+
return [
|
|
6554
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'MIN_LENGTH', `至少需要 ${issue.limit} 個字`, issue.limit),
|
|
6555
|
+
];
|
|
6297
6556
|
}
|
|
6298
6557
|
if (issue.kind === 'maxLength') {
|
|
6299
|
-
return [
|
|
6558
|
+
return [
|
|
6559
|
+
(0, form_data_issue_1.createFormDataIssue)(location, 'MAX_LENGTH', `最多 ${issue.limit} 個字`, issue.limit),
|
|
6560
|
+
];
|
|
6300
6561
|
}
|
|
6301
|
-
return [
|
|
6562
|
+
return [(0, form_data_issue_1.createFormDataIssue)(location, 'PATTERN', '格式不正確')];
|
|
6302
6563
|
}
|
|
6303
6564
|
/**
|
|
6304
6565
|
* Own-property read. `constructor`, `toString` and friends all satisfy the
|