@rytass/bpm-core-nestjs-module 0.10.0 โ 0.12.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 +46 -0
- package/package.json +3 -3
- package/src/lib/condition/condition.service.js +4 -1
- package/src/lib/condition/condition.service.js.map +1 -1
- package/src/lib/form/form-schema.validator.js +277 -56
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/form/form-table-reference.d.ts +28 -0
- package/src/lib/form/form-table-reference.js +53 -0
- package/src/lib/form/form-table-reference.js.map +1 -0
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js +81 -13
- 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.queries.d.ts +4 -0
- package/src/lib/form-data-source/form-data-source.queries.js +31 -0
- package/src/lib/form-data-source/form-data-source.queries.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.service.d.ts +21 -0
- package/src/lib/form-data-source/form-data-source.service.js +70 -17
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.types.d.ts +6 -0
- package/src/lib/form-data-source/form-data-source.types.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.validation.d.ts +12 -1
- package/src/lib/form-data-source/form-data-source.validation.js +34 -5
- package/src/lib/form-data-source/form-data-source.validation.js.map +1 -1
- package/src/lib/notification/notification-delivery.service.js +70 -8
- package/src/lib/notification/notification-delivery.service.js.map +1 -1
- package/src/lib/notification/notification-options.d.ts +13 -0
- package/src/lib/notification/notification-options.js +13 -0
- package/src/lib/notification/notification-options.js.map +1 -1
- package/src/lib/template/template.service.js +34 -1
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-condition-evaluator.js +18 -0
- package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.js +119 -6
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## 0.12.0 (2026-08-27)
|
|
2
|
+
|
|
3
|
+
### ๐ Features
|
|
4
|
+
|
|
5
|
+
- **form:** wire cell-level DataSources through the client and renderer ([c7561ad](https://github.com/Rytass/BPMCore/commit/c7561ad))
|
|
6
|
+
- **form:** resolve table column DataSources per cell ([ca40a4c](https://github.com/Rytass/BPMCore/commit/ca40a4c))
|
|
7
|
+
- **form:** add table column factories and row-scoped binding rename ([0c8c07d](https://github.com/Rytass/BPMCore/commit/0c8c07d))
|
|
8
|
+
- **form:** validate and seed table values in the renderer helpers ([5c7edab](https://github.com/Rytass/BPMCore/commit/5c7edab))
|
|
9
|
+
- **template:** reject published conditions that address table internals ([04d74e3](https://github.com/Rytass/BPMCore/commit/04d74e3))
|
|
10
|
+
- **form:** validate submitted table form data ([69036d1](https://github.com/Rytass/BPMCore/commit/69036d1))
|
|
11
|
+
- **form:** lint table field structure before publishing ([d9e95ee](https://github.com/Rytass/BPMCore/commit/d9e95ee))
|
|
12
|
+
- **condition:** compile table emptiness from the row count ([624c96d](https://github.com/Rytass/BPMCore/commit/624c96d))
|
|
13
|
+
- **form:** add table field definitions to the shared contract ([5f97165](https://github.com/Rytass/BPMCore/commit/5f97165))
|
|
14
|
+
|
|
15
|
+
### ๐ฉน Fixes
|
|
16
|
+
|
|
17
|
+
- **form:** keep a table's row indexes stable past a malformed row ([21d5a57](https://github.com/Rytass/BPMCore/commit/21d5a57))
|
|
18
|
+
- **form:** read required DataSource parameters as own properties ([a5895a7](https://github.com/Rytass/BPMCore/commit/a5895a7))
|
|
19
|
+
- **form:** report a table column's binding lint as an actionable binding error ([ed4fed2](https://github.com/Rytass/BPMCore/commit/ed4fed2))
|
|
20
|
+
- **condition:** never match an edge value comparison against row records ([58b4194](https://github.com/Rytass/BPMCore/commit/58b4194))
|
|
21
|
+
- **workflow:** no-op indexed set-form-field paths and read cells as own properties ([952b7bf](https://github.com/Rytass/BPMCore/commit/952b7bf))
|
|
22
|
+
- **template:** close the whitespace gap in the table-internals condition lint ([6d15d07](https://github.com/Rytass/BPMCore/commit/6d15d07))
|
|
23
|
+
- **condition:** keep table values from deciding form conditions ([894ef00](https://github.com/Rytass/BPMCore/commit/894ef00))
|
|
24
|
+
- **workflow:** stop nested form writes from replacing a non-record value ([336951c](https://github.com/Rytass/BPMCore/commit/336951c))
|
|
25
|
+
|
|
26
|
+
### โค๏ธ Thank You
|
|
27
|
+
|
|
28
|
+
- Chia Yu Pai @fantasywind
|
|
29
|
+
- Claude Opus 5 (1M context)
|
|
30
|
+
|
|
31
|
+
## 0.11.0 (2026-08-18)
|
|
32
|
+
|
|
33
|
+
### ๐ Features
|
|
34
|
+
|
|
35
|
+
- **notification:** bound delivery dispatch with a timeout ([d8115f0](https://github.com/Rytass/BPMCore/commit/d8115f0))
|
|
36
|
+
|
|
37
|
+
### ๐ฉน Fixes
|
|
38
|
+
|
|
39
|
+
- **notification:** read claimed ids from the UPDATE result pair ([2fe6e44](https://github.com/Rytass/BPMCore/commit/2fe6e44))
|
|
40
|
+
- **notification:** move nodemailer to ^9.0.1 ([e094738](https://github.com/Rytass/BPMCore/commit/e094738))
|
|
41
|
+
|
|
42
|
+
### โค๏ธ Thank You
|
|
43
|
+
|
|
44
|
+
- Chia Yu Pai @fantasywind
|
|
45
|
+
- Claude Opus 5
|
|
46
|
+
|
|
1
47
|
## 0.10.0 (2026-08-18)
|
|
2
48
|
|
|
3
49
|
### ๐ Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rytass/bpm-core-nestjs-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Embeddable NestJS BPM approval workflow module: workflow engine, approval templates, forms, organization/member contracts, attachments, signatures, notifications, delegation, and TypeORM migrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bpm",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"@nestjs/core": ">=10.0.0",
|
|
156
156
|
"@nestjs/graphql": ">=12.0.0",
|
|
157
157
|
"@nestjs/typeorm": ">=10.0.0",
|
|
158
|
-
"@rytass/bpm-core-shared": "^0.
|
|
158
|
+
"@rytass/bpm-core-shared": "^0.12.0",
|
|
159
159
|
"@rytass/secret-adapter-vault-nestjs": ">=0.4.5",
|
|
160
160
|
"express": ">=4.0.0",
|
|
161
161
|
"graphql": ">=16.0.0",
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"class-transformer": "^0.5.1",
|
|
178
178
|
"class-validator": "^0.15.1",
|
|
179
179
|
"handlebars": "^4.7.9",
|
|
180
|
-
"nodemailer": "^
|
|
180
|
+
"nodemailer": "^9.0.1",
|
|
181
181
|
"tslib": "^2.3.0"
|
|
182
182
|
}
|
|
183
183
|
}
|
|
@@ -5,7 +5,10 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const common_1 = require("@nestjs/common");
|
|
6
6
|
const cel_js_1 = require("cel-js");
|
|
7
7
|
const DEFAULT_MAX_EXPRESSION_LENGTH = 2000;
|
|
8
|
-
|
|
8
|
+
// `has` and `size` are CEL macros, not context identifiers; the root-identifier
|
|
9
|
+
// lint must not report them as unknown. `size` is required by the table field
|
|
10
|
+
// IS_FILLED / IS_EMPTY expressions (ADR 16 ยง3.8).
|
|
11
|
+
const CEL_KEYWORDS = new Set(['false', 'has', 'in', 'null', 'size', 'true']);
|
|
9
12
|
let ConditionService = class ConditionService {
|
|
10
13
|
lintExpressions(expressions, options = {}) {
|
|
11
14
|
return expressions.flatMap(({ expression, label }) => this.lintExpression(expression, label, options));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"condition.service.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/condition/condition.service.ts"],"names":[],"mappings":";;;;AAAA,2CAAiE;AACjE,mCAAyE;AAczE,MAAM,6BAA6B,GAAG,IAAI,CAAC;AAC3C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"condition.service.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/condition/condition.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ exports.EMPTY_FORM_UI_SCHEMA = exports.EMPTY_FORM_SCHEMA = void 0;
|
|
|
4
4
|
exports.parseAndValidateFormSchemas = parseAndValidateFormSchemas;
|
|
5
5
|
exports.lintFormSchemaJson = lintFormSchemaJson;
|
|
6
6
|
const form_1 = require("@rytass/bpm-core-shared/form");
|
|
7
|
+
const form_table_reference_1 = require("./form-table-reference");
|
|
7
8
|
const SUPPORTED_FIELD_TYPES = [
|
|
8
9
|
'boolean',
|
|
9
10
|
'checkbox',
|
|
@@ -15,6 +16,7 @@ const SUPPORTED_FIELD_TYPES = [
|
|
|
15
16
|
'autocomplete',
|
|
16
17
|
'radio',
|
|
17
18
|
'select',
|
|
19
|
+
'table',
|
|
18
20
|
'text',
|
|
19
21
|
'textarea',
|
|
20
22
|
];
|
|
@@ -37,6 +39,11 @@ const DATE_FIELD_TYPES = new Set([
|
|
|
37
39
|
'datetime',
|
|
38
40
|
]);
|
|
39
41
|
const UI_WIDTHS = new Set(['FULL', 'HALF', 'THIRD']);
|
|
42
|
+
const CONDITION_FIELD_KEYS = [
|
|
43
|
+
'visibleWhen',
|
|
44
|
+
'requiredWhen',
|
|
45
|
+
'readonlyWhen',
|
|
46
|
+
];
|
|
40
47
|
exports.EMPTY_FORM_SCHEMA = {
|
|
41
48
|
fields: [],
|
|
42
49
|
schemaVersion: 1,
|
|
@@ -105,7 +112,7 @@ function parseJsonValue(value, label) {
|
|
|
105
112
|
}
|
|
106
113
|
function lintFormSchemaValues(schema, uiSchema) {
|
|
107
114
|
const schemaErrors = lintDefinitionSchema(schema);
|
|
108
|
-
const uiSchemaErrors = lintUiSchema(uiSchema, readFieldKeySet(schema));
|
|
115
|
+
const uiSchemaErrors = lintUiSchema(uiSchema, readFieldKeySet(schema), readTableFieldKeySet(schema));
|
|
109
116
|
const errors = [...schemaErrors, ...uiSchemaErrors];
|
|
110
117
|
return {
|
|
111
118
|
errors,
|
|
@@ -121,8 +128,13 @@ function lintDefinitionSchema(schema) {
|
|
|
121
128
|
if (!Array.isArray(fields)) {
|
|
122
129
|
return [...versionErrors, 'schema.fields must be an array'];
|
|
123
130
|
}
|
|
124
|
-
const
|
|
125
|
-
const
|
|
131
|
+
const tableFieldKeys = readTableFieldKeySet(schema);
|
|
132
|
+
const scope = {
|
|
133
|
+
columnKeys: null,
|
|
134
|
+
scalarFieldKeys: readScalarFieldKeySet(schema),
|
|
135
|
+
tableFieldKeys,
|
|
136
|
+
};
|
|
137
|
+
const fieldErrors = fields.flatMap((field, index) => lintFieldDefinition(field, index, scope));
|
|
126
138
|
const duplicateErrors = readDuplicateFieldKeyErrors(fields);
|
|
127
139
|
const dependencyErrors = lintFieldDependencyGraph(fields);
|
|
128
140
|
return [
|
|
@@ -132,46 +144,80 @@ function lintDefinitionSchema(schema) {
|
|
|
132
144
|
...dependencyErrors,
|
|
133
145
|
];
|
|
134
146
|
}
|
|
135
|
-
function lintFieldDefinition(field, index,
|
|
147
|
+
function lintFieldDefinition(field, index, scope) {
|
|
136
148
|
if (!isRecord(field)) {
|
|
137
149
|
return [`schema.fields[${index}] must be an object`];
|
|
138
150
|
}
|
|
151
|
+
const path = `schema.fields[${index}]`;
|
|
139
152
|
const type = field.type;
|
|
140
153
|
const basicErrors = [
|
|
141
|
-
...
|
|
142
|
-
...
|
|
143
|
-
...lintOptionalString(field.
|
|
144
|
-
...lintOptionalString(field.
|
|
145
|
-
...
|
|
146
|
-
...
|
|
147
|
-
...
|
|
148
|
-
...(typeof field.required === 'boolean'
|
|
149
|
-
? []
|
|
150
|
-
: [`schema.fields[${index}].required must be a boolean`]),
|
|
151
|
-
...(isSupportedFieldType(type)
|
|
152
|
-
? []
|
|
153
|
-
: [`schema.fields[${index}].type is not supported`]),
|
|
154
|
+
...lintFieldIdentity(field, path),
|
|
155
|
+
...lintOptionalString(field.visibleWhen, `${path}.visibleWhen`),
|
|
156
|
+
...lintOptionalString(field.requiredWhen, `${path}.requiredWhen`),
|
|
157
|
+
...lintOptionalString(field.readonlyWhen, `${path}.readonlyWhen`),
|
|
158
|
+
...lintFieldRequiredFlag(field, path),
|
|
159
|
+
...(isSupportedFieldType(type) ? [] : [`${path}.type is not supported`]),
|
|
160
|
+
...lintConditionTableReferences(field, path, scope.tableFieldKeys),
|
|
154
161
|
];
|
|
155
162
|
if (!isSupportedFieldType(type)) {
|
|
156
163
|
return basicErrors;
|
|
157
164
|
}
|
|
158
|
-
|
|
165
|
+
return [...basicErrors, ...lintFieldTypeSpecifics(field, type, path, scope)];
|
|
166
|
+
}
|
|
167
|
+
function lintFieldTypeSpecifics(field, type, path, scope) {
|
|
159
168
|
if (TEXT_FIELD_TYPES.has(type)) {
|
|
160
|
-
return
|
|
169
|
+
return lintTextField(field, path);
|
|
161
170
|
}
|
|
162
171
|
if (NUMBER_FIELD_TYPES.has(type)) {
|
|
163
|
-
return
|
|
172
|
+
return lintNumberField(field, path);
|
|
164
173
|
}
|
|
165
174
|
if (DATE_FIELD_TYPES.has(type)) {
|
|
166
|
-
return
|
|
175
|
+
return lintDateField(field, path);
|
|
167
176
|
}
|
|
168
177
|
if (SELECT_FIELD_TYPES.has(type)) {
|
|
169
|
-
return
|
|
178
|
+
return lintSelectField(field, type, path, scope);
|
|
170
179
|
}
|
|
171
180
|
if (type === 'boolean') {
|
|
172
|
-
return
|
|
181
|
+
return lintBooleanField(field, path);
|
|
182
|
+
}
|
|
183
|
+
if (type === 'table') {
|
|
184
|
+
return lintTableField(field, path, scope);
|
|
185
|
+
}
|
|
186
|
+
return lintFileUploadField(field, path);
|
|
187
|
+
}
|
|
188
|
+
function lintFieldIdentity(field, path) {
|
|
189
|
+
return [
|
|
190
|
+
...lintRequiredString(field.fieldKey, `${path}.fieldKey`),
|
|
191
|
+
...lintRequiredString(field.label, `${path}.label`),
|
|
192
|
+
...lintOptionalString(field.description, `${path}.description`),
|
|
193
|
+
...lintOptionalString(field.placeholder, `${path}.placeholder`),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
function lintFieldRequiredFlag(field, path) {
|
|
197
|
+
return typeof field.required === 'boolean'
|
|
198
|
+
? []
|
|
199
|
+
: [`${path}.required must be a boolean`];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Form-level conditions cannot address a cell: neither the frontend nor the
|
|
203
|
+
* backend condition parser understands bracket paths, so referencing table
|
|
204
|
+
* internals is rejected outright instead of relying on a parse failure
|
|
205
|
+
* (ADR 16 ยง3.8). Referencing the table itself stays legal โ that is what the
|
|
206
|
+
* IS_FILLED / IS_EMPTY operators compile to.
|
|
207
|
+
*/
|
|
208
|
+
function lintConditionTableReferences(field, path, tableFieldKeys) {
|
|
209
|
+
if (!tableFieldKeys.size) {
|
|
210
|
+
return [];
|
|
173
211
|
}
|
|
174
|
-
return
|
|
212
|
+
return CONDITION_FIELD_KEYS.flatMap((conditionKey) => {
|
|
213
|
+
const expression = field[conditionKey];
|
|
214
|
+
if (typeof expression !== 'string') {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
return [...tableFieldKeys]
|
|
218
|
+
.filter((tableKey) => (0, form_table_reference_1.referencesTableInternals)(expression, tableKey))
|
|
219
|
+
.map((tableKey) => `${path}.${conditionKey} must not reference table field internals: ${tableKey}`);
|
|
220
|
+
});
|
|
175
221
|
}
|
|
176
222
|
function lintTextField(field, path) {
|
|
177
223
|
const minLengthErrors = lintOptionalInteger(field.minLength, `${path}.minLength`, 0);
|
|
@@ -206,8 +252,8 @@ function lintNumberField(field, path) {
|
|
|
206
252
|
function lintDateField(field, path) {
|
|
207
253
|
return lintOptionalDefaultValue(field.defaultValue, path, (value) => typeof value === 'string' || value === null);
|
|
208
254
|
}
|
|
209
|
-
function lintSelectField(field, type, path,
|
|
210
|
-
const sourceErrors = lintOptionSource(field, type, path,
|
|
255
|
+
function lintSelectField(field, type, path, scope) {
|
|
256
|
+
const sourceErrors = lintOptionSource(field, type, path, scope);
|
|
211
257
|
const hasDataSource = Object.prototype.hasOwnProperty.call(field, 'dataSource');
|
|
212
258
|
return [
|
|
213
259
|
...sourceErrors,
|
|
@@ -216,7 +262,7 @@ function lintSelectField(field, type, path, fieldKeys) {
|
|
|
216
262
|
: lintSelectDefaultValue(field, type, path)),
|
|
217
263
|
];
|
|
218
264
|
}
|
|
219
|
-
function lintOptionSource(field, type, path,
|
|
265
|
+
function lintOptionSource(field, type, path, scope) {
|
|
220
266
|
const hasOptions = Object.prototype.hasOwnProperty.call(field, 'options');
|
|
221
267
|
const hasDataSource = Object.prototype.hasOwnProperty.call(field, 'dataSource');
|
|
222
268
|
const sourceCount = Number(hasOptions) + Number(hasDataSource);
|
|
@@ -239,7 +285,7 @@ function lintOptionSource(field, type, path, fieldKeys) {
|
|
|
239
285
|
return [
|
|
240
286
|
...sourceShapeErrors,
|
|
241
287
|
...modeErrors,
|
|
242
|
-
...lintDataSourceReference(field.dataSource, `${path}.dataSource`,
|
|
288
|
+
...lintDataSourceReference(field.dataSource, `${path}.dataSource`, scope),
|
|
243
289
|
];
|
|
244
290
|
}
|
|
245
291
|
function lintOptionMode(field, type, path) {
|
|
@@ -256,7 +302,7 @@ function lintOptionMode(field, type, path) {
|
|
|
256
302
|
? []
|
|
257
303
|
: [`${path}.mode must be single or multiple`];
|
|
258
304
|
}
|
|
259
|
-
function lintDataSourceReference(value, path,
|
|
305
|
+
function lintDataSourceReference(value, path, scope) {
|
|
260
306
|
if (!isRecord(value)) {
|
|
261
307
|
return [`${path} must be an object`];
|
|
262
308
|
}
|
|
@@ -268,7 +314,7 @@ function lintDataSourceReference(value, path, fieldKeys) {
|
|
|
268
314
|
if (!Array.isArray(bindings)) {
|
|
269
315
|
return [...referenceErrors, `${path}.bindings must be an array`];
|
|
270
316
|
}
|
|
271
|
-
const bindingErrors = bindings.flatMap((binding, index) => lintDataSourceBinding(binding, `${path}.bindings[${index}]`,
|
|
317
|
+
const bindingErrors = bindings.flatMap((binding, index) => lintDataSourceBinding(binding, `${path}.bindings[${index}]`, scope));
|
|
272
318
|
const parameters = bindings
|
|
273
319
|
.filter(isRecord)
|
|
274
320
|
.map((binding) => binding.parameter)
|
|
@@ -282,7 +328,7 @@ function lintDataSourceReference(value, path, fieldKeys) {
|
|
|
282
328
|
...duplicateParameters.map((parameter) => `${path}.bindings.parameter is duplicated: ${parameter}`),
|
|
283
329
|
];
|
|
284
330
|
}
|
|
285
|
-
function lintDataSourceBinding(value, path,
|
|
331
|
+
function lintDataSourceBinding(value, path, scope) {
|
|
286
332
|
if (!isRecord(value)) {
|
|
287
333
|
return [`${path} must be an object`];
|
|
288
334
|
}
|
|
@@ -295,9 +341,7 @@ function lintDataSourceBinding(value, path, fieldKeys) {
|
|
|
295
341
|
return [
|
|
296
342
|
...errors,
|
|
297
343
|
...lintRequiredString(from.fieldKey, `${path}.from.fieldKey`),
|
|
298
|
-
...(
|
|
299
|
-
? [`${path}.from.fieldKey does not match a schema field`]
|
|
300
|
-
: []),
|
|
344
|
+
...lintFieldBindingTarget(from.fieldKey, `${path}.from.fieldKey`, scope),
|
|
301
345
|
];
|
|
302
346
|
}
|
|
303
347
|
if (from.kind === 'CONSTANT') {
|
|
@@ -306,7 +350,37 @@ function lintDataSourceBinding(value, path, fieldKeys) {
|
|
|
306
350
|
...lintConstantValue(from.value, `${path}.from.value`),
|
|
307
351
|
];
|
|
308
352
|
}
|
|
309
|
-
|
|
353
|
+
if (from.kind === 'ROW_FIELD') {
|
|
354
|
+
const columnKeys = scope.columnKeys;
|
|
355
|
+
if (!columnKeys) {
|
|
356
|
+
return [
|
|
357
|
+
...errors,
|
|
358
|
+
`${path}.from.kind ROW_FIELD is only supported inside a table column`,
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
return [
|
|
362
|
+
...errors,
|
|
363
|
+
...lintRequiredString(from.columnKey, `${path}.from.columnKey`),
|
|
364
|
+
...(typeof from.columnKey === 'string' && !columnKeys.has(from.columnKey)
|
|
365
|
+
? [`${path}.from.columnKey does not match a table column`]
|
|
366
|
+
: []),
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
return [...errors, `${path}.from.kind must be FIELD, CONSTANT, or ROW_FIELD`];
|
|
370
|
+
}
|
|
371
|
+
function lintFieldBindingTarget(fieldKey, path, scope) {
|
|
372
|
+
if (typeof fieldKey !== 'string') {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
// A table value is a list of rows, so it can never stand in for a scalar
|
|
376
|
+
// data source parameter โ and cross-row references have no single-value
|
|
377
|
+
// semantics in V1 (ADR 16 ยง3.4).
|
|
378
|
+
if (scope.tableFieldKeys.has(fieldKey)) {
|
|
379
|
+
return [`${path} must not reference a table field`];
|
|
380
|
+
}
|
|
381
|
+
return scope.scalarFieldKeys.has(fieldKey)
|
|
382
|
+
? []
|
|
383
|
+
: [`${path} does not match a schema field`];
|
|
310
384
|
}
|
|
311
385
|
function lintConstantValue(value, path) {
|
|
312
386
|
return value === null ||
|
|
@@ -324,6 +398,100 @@ function lintDynamicDefaultValue(field, path) {
|
|
|
324
398
|
function lintBooleanField(field, path) {
|
|
325
399
|
return lintOptionalDefaultValue(field.defaultValue, path, (value) => typeof value === 'boolean' || value === null);
|
|
326
400
|
}
|
|
401
|
+
function lintTableField(field, path, scope) {
|
|
402
|
+
const structureErrors = [
|
|
403
|
+
...lintIdentifierKey(field.fieldKey, `${path}.fieldKey`),
|
|
404
|
+
...lintOptionalString(field.addRowLabel, `${path}.addRowLabel`),
|
|
405
|
+
...lintTableRowBounds(field, path),
|
|
406
|
+
...(Object.prototype.hasOwnProperty.call(field, 'defaultValue')
|
|
407
|
+
? [`${path}.defaultValue is not supported for table fields`]
|
|
408
|
+
: []),
|
|
409
|
+
];
|
|
410
|
+
const columns = field.columns;
|
|
411
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
412
|
+
return [...structureErrors, `${path}.columns must contain at least one column`];
|
|
413
|
+
}
|
|
414
|
+
const columnKeys = columns
|
|
415
|
+
.filter(isRecord)
|
|
416
|
+
.map((column) => column.fieldKey)
|
|
417
|
+
.filter((columnKey) => typeof columnKey === 'string');
|
|
418
|
+
const columnScope = {
|
|
419
|
+
...scope,
|
|
420
|
+
columnKeys: new Set(columnKeys),
|
|
421
|
+
};
|
|
422
|
+
const duplicatedColumnKeys = [
|
|
423
|
+
...new Set(columnKeys.filter((columnKey, index) => columnKeys.indexOf(columnKey) !== index)),
|
|
424
|
+
];
|
|
425
|
+
return [
|
|
426
|
+
...structureErrors,
|
|
427
|
+
...columns.flatMap((column, index) => lintTableColumn(column, `${path}.columns[${index}]`, columnScope)),
|
|
428
|
+
...duplicatedColumnKeys.map((columnKey) => `${path}.columns fieldKey is duplicated: ${columnKey}`),
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
function lintTableRowBounds(field, path) {
|
|
432
|
+
const minRowsErrors = lintOptionalInteger(field.minRows, `${path}.minRows`, 0);
|
|
433
|
+
const maxRowsErrors = lintOptionalInteger(field.maxRows, `${path}.maxRows`, 1);
|
|
434
|
+
const ceilingErrors = typeof field.maxRows === 'number' && field.maxRows > form_1.FORM_TABLE_MAX_ROWS
|
|
435
|
+
? [
|
|
436
|
+
`${path}.maxRows must be less than or equal to ${form_1.FORM_TABLE_MAX_ROWS}`,
|
|
437
|
+
]
|
|
438
|
+
: [];
|
|
439
|
+
const rangeErrors = typeof field.minRows === 'number' &&
|
|
440
|
+
typeof field.maxRows === 'number' &&
|
|
441
|
+
field.minRows > field.maxRows
|
|
442
|
+
? [`${path}.minRows must be less than or equal to ${path}.maxRows`]
|
|
443
|
+
: [];
|
|
444
|
+
const implicitCeilingErrors = typeof field.minRows === 'number' &&
|
|
445
|
+
typeof field.maxRows === 'undefined' &&
|
|
446
|
+
field.minRows > form_1.FORM_TABLE_MAX_ROWS
|
|
447
|
+
? [
|
|
448
|
+
`${path}.minRows must be less than or equal to ${form_1.FORM_TABLE_MAX_ROWS}`,
|
|
449
|
+
]
|
|
450
|
+
: [];
|
|
451
|
+
return [
|
|
452
|
+
...minRowsErrors,
|
|
453
|
+
...maxRowsErrors,
|
|
454
|
+
...ceilingErrors,
|
|
455
|
+
...rangeErrors,
|
|
456
|
+
...implicitCeilingErrors,
|
|
457
|
+
];
|
|
458
|
+
}
|
|
459
|
+
function lintTableColumn(column, path, scope) {
|
|
460
|
+
if (!isRecord(column)) {
|
|
461
|
+
return [`${path} must be an object`];
|
|
462
|
+
}
|
|
463
|
+
const type = column.type;
|
|
464
|
+
const basicErrors = [
|
|
465
|
+
...lintFieldIdentity(column, path),
|
|
466
|
+
...lintIdentifierKey(column.fieldKey, `${path}.fieldKey`),
|
|
467
|
+
...CONDITION_FIELD_KEYS.filter((conditionKey) => Object.prototype.hasOwnProperty.call(column, conditionKey)).map((conditionKey) => `${path}.${conditionKey} is not supported for table columns`),
|
|
468
|
+
...lintFieldRequiredFlag(column, path),
|
|
469
|
+
...lintTableColumnType(type, path),
|
|
470
|
+
];
|
|
471
|
+
if (!isSupportedFieldType(type) || !(0, form_1.isTableColumnFieldType)(type)) {
|
|
472
|
+
return basicErrors;
|
|
473
|
+
}
|
|
474
|
+
return [...basicErrors, ...lintFieldTypeSpecifics(column, type, path, scope)];
|
|
475
|
+
}
|
|
476
|
+
function lintTableColumnType(type, path) {
|
|
477
|
+
if (type === 'table') {
|
|
478
|
+
return [`${path}.type must not be a nested table`];
|
|
479
|
+
}
|
|
480
|
+
if (!isSupportedFieldType(type)) {
|
|
481
|
+
return [`${path}.type is not supported`];
|
|
482
|
+
}
|
|
483
|
+
return (0, form_1.isTableColumnFieldType)(type)
|
|
484
|
+
? []
|
|
485
|
+
: [`${path}.type is not supported for a table column`];
|
|
486
|
+
}
|
|
487
|
+
function lintIdentifierKey(value, path) {
|
|
488
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
return (0, form_1.isFormIdentifierKey)(value)
|
|
492
|
+
? []
|
|
493
|
+
: [`${path} must match /^[A-Za-z_][A-Za-z0-9_]*$/`];
|
|
494
|
+
}
|
|
327
495
|
function lintFileUploadField(field, path) {
|
|
328
496
|
return [
|
|
329
497
|
...lintOptionalInteger(field.maxFiles, `${path}.maxFiles`, 1),
|
|
@@ -378,7 +546,7 @@ function lintOptionalDefaultValue(value, path, predicate) {
|
|
|
378
546
|
}
|
|
379
547
|
return [`${path}.defaultValue has invalid type for field type`];
|
|
380
548
|
}
|
|
381
|
-
function lintUiSchema(uiSchema, fieldKeys) {
|
|
549
|
+
function lintUiSchema(uiSchema, fieldKeys, tableFieldKeys) {
|
|
382
550
|
if (!isRecord(uiSchema)) {
|
|
383
551
|
return ['uiSchema must be an object'];
|
|
384
552
|
}
|
|
@@ -389,10 +557,10 @@ function lintUiSchema(uiSchema, fieldKeys) {
|
|
|
389
557
|
}
|
|
390
558
|
return [
|
|
391
559
|
...versionErrors,
|
|
392
|
-
...layout.flatMap((item, index) => lintLayoutItem(item, index, fieldKeys)),
|
|
560
|
+
...layout.flatMap((item, index) => lintLayoutItem(item, index, fieldKeys, tableFieldKeys)),
|
|
393
561
|
];
|
|
394
562
|
}
|
|
395
|
-
function lintLayoutItem(item, index, fieldKeys) {
|
|
563
|
+
function lintLayoutItem(item, index, fieldKeys, tableFieldKeys) {
|
|
396
564
|
if (!isRecord(item)) {
|
|
397
565
|
return [`uiSchema.layout[${index}] must be an object`];
|
|
398
566
|
}
|
|
@@ -406,6 +574,11 @@ function lintLayoutItem(item, index, fieldKeys) {
|
|
|
406
574
|
...(isUiWidth(width)
|
|
407
575
|
? []
|
|
408
576
|
: [`uiSchema.layout[${index}].width must be FULL, HALF, or THIRD`]),
|
|
577
|
+
...(typeof fieldKey === 'string' &&
|
|
578
|
+
tableFieldKeys.has(fieldKey) &&
|
|
579
|
+
width !== 'FULL'
|
|
580
|
+
? [`uiSchema.layout[${index}].width must be FULL for table fields`]
|
|
581
|
+
: []),
|
|
409
582
|
];
|
|
410
583
|
}
|
|
411
584
|
function readDuplicateFieldKeyErrors(fields) {
|
|
@@ -417,30 +590,68 @@ function readDuplicateFieldKeyErrors(fields) {
|
|
|
417
590
|
const uniqueDuplicated = [...new Set(duplicated)];
|
|
418
591
|
return uniqueDuplicated.map((key) => `schema.fields fieldKey is duplicated: ${key}`);
|
|
419
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Two layers in one graph (ADR 16 ยง3.4): top-level fields joined by `FIELD`
|
|
595
|
+
* bindings, and each table's columns joined by `ROW_FIELD` bindings plus their
|
|
596
|
+
* `FIELD` edges up to the top level. Node ids are prefixed so a top-level key
|
|
597
|
+
* that happens to contain a dot cannot collide with a column node.
|
|
598
|
+
*/
|
|
420
599
|
function lintFieldDependencyGraph(fields) {
|
|
421
|
-
const dependencies = fields.
|
|
422
|
-
if (!isRecord(field) || typeof field.fieldKey !== 'string') {
|
|
423
|
-
return graph;
|
|
424
|
-
}
|
|
425
|
-
const nextDependencies = isRecord(field.dataSource) &&
|
|
426
|
-
Array.isArray(field.dataSource.bindings)
|
|
427
|
-
? field.dataSource.bindings
|
|
428
|
-
.filter(isRecord)
|
|
429
|
-
.map((binding) => binding.from)
|
|
430
|
-
.filter(isRecord)
|
|
431
|
-
.filter((from) => from.kind === 'FIELD')
|
|
432
|
-
.map((from) => from.fieldKey)
|
|
433
|
-
.filter((fieldKey) => typeof fieldKey === 'string')
|
|
434
|
-
: [];
|
|
435
|
-
return new Map(graph).set(field.fieldKey, nextDependencies);
|
|
436
|
-
}, new Map());
|
|
600
|
+
const dependencies = new Map(fields.filter(isRecord).flatMap(readFieldDependencyEntries));
|
|
437
601
|
const cycles = [...dependencies.keys()]
|
|
438
|
-
.map((
|
|
602
|
+
.map((node) => findDependencyCycle(node, dependencies, []))
|
|
439
603
|
.filter((cycle) => Boolean(cycle))
|
|
440
|
-
.map((cycle) => cycle.join(' -> '));
|
|
604
|
+
.map((cycle) => cycle.map(readDependencyNodeLabel).join(' -> '));
|
|
441
605
|
const uniqueCycles = [...new Set(cycles)];
|
|
442
606
|
return uniqueCycles.map((cycle) => `schema.fields dependency cycle: ${cycle}`);
|
|
443
607
|
}
|
|
608
|
+
function readFieldDependencyEntries(field) {
|
|
609
|
+
if (typeof field.fieldKey !== 'string') {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const fieldKey = field.fieldKey;
|
|
613
|
+
if (field.type === 'table') {
|
|
614
|
+
return Array.isArray(field.columns)
|
|
615
|
+
? field.columns
|
|
616
|
+
.filter(isRecord)
|
|
617
|
+
.filter((column) => typeof column.fieldKey === 'string')
|
|
618
|
+
.map((column) => [
|
|
619
|
+
`col:${fieldKey}.${String(column.fieldKey)}`,
|
|
620
|
+
readBindingSources(column).flatMap((from) => readColumnDependencyNode(from, fieldKey)),
|
|
621
|
+
])
|
|
622
|
+
: [];
|
|
623
|
+
}
|
|
624
|
+
return [
|
|
625
|
+
[
|
|
626
|
+
`top:${fieldKey}`,
|
|
627
|
+
readBindingSources(field)
|
|
628
|
+
.filter((from) => from.kind === 'FIELD')
|
|
629
|
+
.map((from) => from.fieldKey)
|
|
630
|
+
.filter((dependencyKey) => typeof dependencyKey === 'string')
|
|
631
|
+
.map((dependencyKey) => `top:${dependencyKey}`),
|
|
632
|
+
],
|
|
633
|
+
];
|
|
634
|
+
}
|
|
635
|
+
function readColumnDependencyNode(from, tableKey) {
|
|
636
|
+
if (from.kind === 'FIELD' && typeof from.fieldKey === 'string') {
|
|
637
|
+
return [`top:${from.fieldKey}`];
|
|
638
|
+
}
|
|
639
|
+
if (from.kind === 'ROW_FIELD' && typeof from.columnKey === 'string') {
|
|
640
|
+
return [`col:${tableKey}.${from.columnKey}`];
|
|
641
|
+
}
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
function readBindingSources(field) {
|
|
645
|
+
return isRecord(field.dataSource) && Array.isArray(field.dataSource.bindings)
|
|
646
|
+
? field.dataSource.bindings
|
|
647
|
+
.filter(isRecord)
|
|
648
|
+
.map((binding) => binding.from)
|
|
649
|
+
.filter(isRecord)
|
|
650
|
+
: [];
|
|
651
|
+
}
|
|
652
|
+
function readDependencyNodeLabel(node) {
|
|
653
|
+
return node.replace(/^(?:top|col):/u, '');
|
|
654
|
+
}
|
|
444
655
|
function findDependencyCycle(fieldKey, dependencies, path) {
|
|
445
656
|
const cycleStart = path.indexOf(fieldKey);
|
|
446
657
|
if (cycleStart >= 0) {
|
|
@@ -457,11 +668,21 @@ function findDependencyCycle(fieldKey, dependencies, path) {
|
|
|
457
668
|
return null;
|
|
458
669
|
}
|
|
459
670
|
function readFieldKeySet(schema) {
|
|
671
|
+
return readFieldKeySetBy(schema, () => true);
|
|
672
|
+
}
|
|
673
|
+
function readTableFieldKeySet(schema) {
|
|
674
|
+
return readFieldKeySetBy(schema, (field) => field.type === 'table');
|
|
675
|
+
}
|
|
676
|
+
function readScalarFieldKeySet(schema) {
|
|
677
|
+
return readFieldKeySetBy(schema, (field) => field.type !== 'table');
|
|
678
|
+
}
|
|
679
|
+
function readFieldKeySetBy(schema, predicate) {
|
|
460
680
|
if (!isRecord(schema) || !Array.isArray(schema.fields)) {
|
|
461
681
|
return new Set();
|
|
462
682
|
}
|
|
463
683
|
return new Set(schema.fields
|
|
464
684
|
.filter(isRecord)
|
|
685
|
+
.filter(predicate)
|
|
465
686
|
.map((field) => field.fieldKey)
|
|
466
687
|
.filter((fieldKey) => typeof fieldKey === 'string'));
|
|
467
688
|
}
|