@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46
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/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +5 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts +14 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +199 -41
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +65 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +276 -108
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
- package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
- package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +15 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +24 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +84 -6
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +64 -14
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +81 -27
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +1 -1
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +15 -2
- package/dist/helpers/expressions.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +156 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer/Composer.tsx +7 -3
- package/src/components/form/engine/CompactRow.tsx +279 -33
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +995 -12
- package/src/components/form/engine/FormEngine.tsx +377 -94
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +55 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +125 -35
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +389 -1
- package/src/components/form/fields/template/TemplateField.tsx +100 -35
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
- package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
- package/src/components/ticketThread/TicketThread.tsx +7 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/expressions.ts +16 -2
- package/src/helpers/optionUiTypes.ts +161 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +1 -0
|
@@ -10,10 +10,58 @@ import isNaN from 'lodash/isNaN';
|
|
|
10
10
|
import isNumber from 'lodash/isNumber';
|
|
11
11
|
import isObject from 'lodash/isPlainObject';
|
|
12
12
|
import size from 'lodash/size';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
13
|
+
import uniqWith from 'lodash/uniqWith';
|
|
14
|
+
import { fixOperatorValue, getAddress, getProtocol, splitByteSize } from './common';
|
|
15
|
+
import { isOptionInterfaceUiType } from './optionUiTypes';
|
|
16
|
+
import { getListElementValue, getOptionsFromRequiredGroups } from './options';
|
|
17
|
+
import { IProviderType, TVariableActionValue, maybeBuildOptionProvider } from './providerValue';
|
|
15
18
|
import { getTemplateKey, getTemplateValue, isValueTemplate } from './templates';
|
|
16
19
|
|
|
20
|
+
/** The five cron fields, in order, as a schedule hash may name them. */
|
|
21
|
+
const CRON_FIELDS: [string, string][] = [
|
|
22
|
+
['minutes', 'minute'],
|
|
23
|
+
['hours', 'hour'],
|
|
24
|
+
['days', 'day'],
|
|
25
|
+
['months', 'month'],
|
|
26
|
+
['dow', 'weekday'],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A five-field cron expression, from either representation a caller may hold.
|
|
31
|
+
*
|
|
32
|
+
* A schedule reaches the form in one of two shapes, and both are legitimate: a
|
|
33
|
+
* joined string (`"0 0 1 1 *"`), or the hash a Qorus job carries
|
|
34
|
+
* (`{minutes, hours, days, months, dow}` — also accepted under the singular
|
|
35
|
+
* `minute`/`hour`/`day`/`month`/`weekday` spellings). The renderer already
|
|
36
|
+
* accepted both; validation did not, and fed the hash straight to
|
|
37
|
+
* `isValidCron`, which calls `.trim()` on it.
|
|
38
|
+
*
|
|
39
|
+
* Returns `undefined` for anything that is neither, so the caller reports an
|
|
40
|
+
* invalid value rather than throwing.
|
|
41
|
+
*/
|
|
42
|
+
export const cronExpressionFromValue = (value: unknown): string | undefined => {
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hash = value as Record<string, unknown>;
|
|
52
|
+
const parts = CRON_FIELDS.map(([plural, singular]) => {
|
|
53
|
+
const field = hash[plural] ?? hash[singular];
|
|
54
|
+
return field === undefined || field === null || field === '' ? '*' : String(field);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// An object with none of the five keys is not a schedule — every part
|
|
58
|
+
// defaulting to `*` would otherwise turn any object at all into "* * * * *"
|
|
59
|
+
return parts.every((part) => part === '*') &&
|
|
60
|
+
!CRON_FIELDS.some(([plural, singular]) => plural in hash || singular in hash)
|
|
61
|
+
? undefined
|
|
62
|
+
: parts.join(' ');
|
|
63
|
+
};
|
|
64
|
+
|
|
17
65
|
export interface IValidationResult {
|
|
18
66
|
isValid: boolean;
|
|
19
67
|
reason?: string;
|
|
@@ -29,17 +77,67 @@ interface IFieldValidationProps {
|
|
|
29
77
|
validation_regex?: string;
|
|
30
78
|
required_groups?: string[];
|
|
31
79
|
optionSchema?: IQorusFormSchema;
|
|
32
|
-
|
|
80
|
+
/** The sibling option VALUES, for required-group and dependency checks.
|
|
81
|
+
* Widened because a schema entry is also passed straight in as `field`, and
|
|
82
|
+
* a schema entry carries an `options` of its own — the field's UI options
|
|
83
|
+
* bag (`{ file?: DropzoneOptions }`) — under the same name. */
|
|
84
|
+
options?: TQorusForm | Record<string, any>;
|
|
33
85
|
isFunction?: boolean;
|
|
34
86
|
arg_schema?: IQorusFormSchema;
|
|
35
87
|
ui_element_type?: string;
|
|
36
88
|
element_type?: string;
|
|
37
89
|
allowed_values?: any[];
|
|
90
|
+
element_allowed_values?: any[];
|
|
38
91
|
disabled?: boolean;
|
|
39
92
|
metadata?: Record<string, any>;
|
|
93
|
+
/** The expression catalogue, when a caller holds one already — see
|
|
94
|
+
* {@link setExpressionCatalogueReader} for where it comes from otherwise. */
|
|
95
|
+
expressions?: any[];
|
|
96
|
+
/** The two key names an `array-of-pairs` row is made of. */
|
|
97
|
+
fields?: string[];
|
|
98
|
+
/** Set for a provider chosen as an FSM variable, which must support an action. */
|
|
99
|
+
isVariable?: boolean;
|
|
100
|
+
/** The declaration a `var-action` is resolved against. Partial because it
|
|
101
|
+
* carries only the provider half — the action supplies the variable half,
|
|
102
|
+
* and the two are merged before the provider validator sees them. */
|
|
103
|
+
variableData?: { value?: Partial<TVariableActionValue> };
|
|
40
104
|
[key: string]: any;
|
|
41
105
|
}
|
|
42
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Where the expression catalogue comes from when a caller does not carry one.
|
|
109
|
+
*
|
|
110
|
+
* Every other rule here is a pure function of the value and its schema, but an
|
|
111
|
+
* expression can only be checked against the catalogue of operations the server
|
|
112
|
+
* publishes — which is application state, fetched once and held in a store. The
|
|
113
|
+
* library must not reach into a consumer's store, so the consumer hands the
|
|
114
|
+
* reader in instead: qorus-ide registers one that reads its expressions store.
|
|
115
|
+
*
|
|
116
|
+
* Unregistered, the catalogue is empty, and `case 'expression'` then validates
|
|
117
|
+
* only what an expression can be judged on by itself (see the ordering there).
|
|
118
|
+
*/
|
|
119
|
+
type TExpressionCatalogueReader = () => any[] | undefined;
|
|
120
|
+
|
|
121
|
+
let expressionCatalogueReader: TExpressionCatalogueReader | undefined;
|
|
122
|
+
|
|
123
|
+
export const setExpressionCatalogueReader = (reader?: TExpressionCatalogueReader): void => {
|
|
124
|
+
expressionCatalogueReader = reader;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const readExpressionCatalogue = (field?: IFieldValidationProps): any[] =>
|
|
128
|
+
field?.expressions ?? expressionCatalogueReader?.() ?? [];
|
|
129
|
+
|
|
130
|
+
// permissive patterns for the semantic string formats — mirror the qore DataProvider
|
|
131
|
+
// QoreStringFormatDataType server backstop (email/uuid/hostname/ipv4/ipv6/phone)
|
|
132
|
+
const SEMANTIC_FORMAT_PATTERNS: Record<string, RegExp> = {
|
|
133
|
+
email: /^[^@\s]+@[^@\s]+$/,
|
|
134
|
+
uuid: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/,
|
|
135
|
+
hostname: /^[A-Za-z0-9.-]+$/,
|
|
136
|
+
ipv4: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
|
137
|
+
ipv6: /^([0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}$/,
|
|
138
|
+
phone: /^\+?[0-9 ().-]{3,}$/,
|
|
139
|
+
};
|
|
140
|
+
|
|
43
141
|
const validResult = (): IValidationResult => ({ isValid: true, reasons: [] });
|
|
44
142
|
|
|
45
143
|
const invalidResult = (
|
|
@@ -115,16 +213,6 @@ export const _validateField = (
|
|
|
115
213
|
if (canBeNull && (value === null || value === undefined)) {
|
|
116
214
|
return validResult();
|
|
117
215
|
}
|
|
118
|
-
// Expression values (`is_expression`) carry the AST `{ exp, args }` instead
|
|
119
|
-
// of a typed literal — validate that an expression is chosen, not the
|
|
120
|
-
// base type. (`isFunction` is set by FormEngine for expression options.)
|
|
121
|
-
if (field?.isFunction) {
|
|
122
|
-
const ast = (value as { value?: any })?.value ?? value;
|
|
123
|
-
return resultFromBoolean(
|
|
124
|
-
!!(ast as { exp?: string })?.exp,
|
|
125
|
-
'Expression operation is required'
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
216
|
// Strip type parameters e.g. hash<string, int> → hash
|
|
129
217
|
const pos: number = type.indexOf('<');
|
|
130
218
|
if (pos > 0) {
|
|
@@ -137,17 +225,44 @@ export const _validateField = (
|
|
|
137
225
|
|
|
138
226
|
return resultFromBoolean(hasKey && hasValue, 'Template must include key and value');
|
|
139
227
|
}
|
|
228
|
+
// Expression values (`is_expression`) carry the AST `{ exp, args }` instead of
|
|
229
|
+
// a typed literal, so the declared type says nothing about them — validate
|
|
230
|
+
// them as the expression they are, arguments and all. (`isFunction` is set by
|
|
231
|
+
// FormEngine, and by the option recursion below, for expression options.)
|
|
232
|
+
if (field?.isFunction) {
|
|
233
|
+
// The AST is normally the value itself; a caller that still holds the
|
|
234
|
+
// option envelope hands over `{ value: ast }`, so accept both.
|
|
235
|
+
const ast = (value as { value?: any })?.value ?? value;
|
|
236
|
+
|
|
237
|
+
return validateFieldWithResult(
|
|
238
|
+
'expression',
|
|
239
|
+
{ value: ast },
|
|
240
|
+
omit(field, ['isFunction']),
|
|
241
|
+
canBeNull
|
|
242
|
+
);
|
|
243
|
+
}
|
|
140
244
|
|
|
141
245
|
// Check if the field has required groups
|
|
142
246
|
if (field?.required_groups) {
|
|
143
247
|
if (
|
|
144
248
|
!isValueDefined(type, value) &&
|
|
145
|
-
validateOptionWithRequiredGroups(
|
|
249
|
+
validateOptionWithRequiredGroups(
|
|
250
|
+
field.options as TQorusForm,
|
|
251
|
+
field.optionSchema,
|
|
252
|
+
field.required_groups
|
|
253
|
+
)
|
|
146
254
|
) {
|
|
147
255
|
return validResult();
|
|
148
256
|
}
|
|
149
257
|
}
|
|
150
258
|
|
|
259
|
+
// An interface type names another Qorus object by its name, so a non-empty
|
|
260
|
+
// string is the whole contract. `connection` is the exception: it carries
|
|
261
|
+
// enablement and authentication state of its own, checked below.
|
|
262
|
+
if (isOptionInterfaceUiType(type) && type !== 'connection') {
|
|
263
|
+
return validateFieldWithResult('string', value, field);
|
|
264
|
+
}
|
|
265
|
+
|
|
151
266
|
switch (type) {
|
|
152
267
|
case 'richtext': {
|
|
153
268
|
if (!value || !size(value)) {
|
|
@@ -200,33 +315,67 @@ export const _validateField = (
|
|
|
200
315
|
'Connection string validation failed'
|
|
201
316
|
);
|
|
202
317
|
}
|
|
318
|
+
// semantic string formats — permissive patterns mirror the qore DataProvider
|
|
319
|
+
// QoreStringFormatDataType backstop; a server-supplied validation_regex, if
|
|
320
|
+
// present, is also enforced
|
|
321
|
+
case 'email':
|
|
322
|
+
case 'uuid':
|
|
323
|
+
case 'hostname':
|
|
324
|
+
case 'ipv4':
|
|
325
|
+
case 'ipv6':
|
|
326
|
+
case 'phone': {
|
|
327
|
+
if (value === undefined || value === null || value === '' || typeof value !== 'string') {
|
|
328
|
+
return invalidResult('Value must be a non-empty text');
|
|
329
|
+
}
|
|
330
|
+
if (field?.validation_regex && !value.match(field.validation_regex)) {
|
|
331
|
+
return invalidResult('Value does not match the required format');
|
|
332
|
+
}
|
|
333
|
+
return resultFromBoolean(
|
|
334
|
+
SEMANTIC_FORMAT_PATTERNS[type].test(value),
|
|
335
|
+
`Value is not a valid ${type}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
203
338
|
case 'binary': {
|
|
204
339
|
if (typeof value !== 'string') {
|
|
205
340
|
return invalidResult('Binary value must be a string');
|
|
206
341
|
}
|
|
207
342
|
|
|
208
|
-
const
|
|
343
|
+
const trimmed = value.trim();
|
|
209
344
|
|
|
210
|
-
if (
|
|
345
|
+
if (trimmed.length === 0) {
|
|
211
346
|
return invalidResult('Binary value is empty');
|
|
212
347
|
}
|
|
213
348
|
|
|
214
|
-
|
|
215
|
-
|
|
349
|
+
// the canonical wire form is base64 (matching the server decode); a "data:<mime>;base64,..." URL
|
|
350
|
+
// and legacy "0x"-prefixed hex are also accepted
|
|
351
|
+
if (/^data:[^;]+;base64,/.test(trimmed)) {
|
|
352
|
+
return validResult();
|
|
216
353
|
}
|
|
217
354
|
|
|
218
|
-
if (
|
|
219
|
-
|
|
355
|
+
if (/^0x/i.test(trimmed)) {
|
|
356
|
+
const hex = trimmed.replace(/^0x/i, '');
|
|
357
|
+
return resultFromBoolean(
|
|
358
|
+
hex.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(hex),
|
|
359
|
+
'Binary hex value is invalid'
|
|
360
|
+
);
|
|
220
361
|
}
|
|
221
362
|
|
|
222
|
-
return
|
|
363
|
+
return resultFromBoolean(
|
|
364
|
+
/^[A-Za-z0-9+/]+={0,2}$/.test(trimmed),
|
|
365
|
+
'Binary value must be base64, a data: URL, or 0x-prefixed hex'
|
|
366
|
+
);
|
|
223
367
|
}
|
|
224
368
|
case 'string':
|
|
225
369
|
case 'mapper':
|
|
226
370
|
case 'workflow':
|
|
227
371
|
case 'service':
|
|
228
372
|
case 'job':
|
|
373
|
+
case 'softstring':
|
|
374
|
+
case 'select-string':
|
|
375
|
+
case 'file-string':
|
|
376
|
+
case 'file-as-string':
|
|
229
377
|
case 'long-string':
|
|
378
|
+
case 'method-name':
|
|
230
379
|
case 'code-editor':
|
|
231
380
|
case 'data': {
|
|
232
381
|
if (value === undefined || value === null || value === '' || typeof value !== 'string') {
|
|
@@ -302,6 +451,128 @@ export const _validateField = (
|
|
|
302
451
|
|
|
303
452
|
return reasons.length ? invalidResult(reasons) : validResult();
|
|
304
453
|
}
|
|
454
|
+
case 'collection-documents': {
|
|
455
|
+
// The value is an array of rows in mixed lifecycle states.
|
|
456
|
+
// A row is valid when it's either:
|
|
457
|
+
// 1. Server-managed: has `ai_documentid` (number) + `name`.
|
|
458
|
+
// 2. Client-managed: has `name` + `content` (base64) — i.e.
|
|
459
|
+
// a fresh upload that hasn't landed yet.
|
|
460
|
+
// Failed rows count as valid for form-level purposes (the
|
|
461
|
+
// user can Retry; the form shouldn't block save just because
|
|
462
|
+
// an upload errored). Empty list is valid here — the
|
|
463
|
+
// top-level required check (`field.required`) handles "must
|
|
464
|
+
// pick at least one" gating.
|
|
465
|
+
if (value === undefined || value === null) {
|
|
466
|
+
return validResult();
|
|
467
|
+
}
|
|
468
|
+
if (!Array.isArray(value)) {
|
|
469
|
+
return invalidResult('Value must be a list of documents');
|
|
470
|
+
}
|
|
471
|
+
const reasons: string[] = [];
|
|
472
|
+
value.forEach((row: any, idx: number) => {
|
|
473
|
+
if (!row || typeof row !== 'object') {
|
|
474
|
+
reasons.push(`Row ${idx + 1} is not an object`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (typeof row.name !== 'string' || !row.name) {
|
|
478
|
+
reasons.push(`Row ${idx + 1} is missing a name`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const hasServerId = typeof row.ai_documentid === 'number';
|
|
482
|
+
const hasContent = typeof row.content === 'string' && !!row.content;
|
|
483
|
+
if (!hasServerId && !hasContent) {
|
|
484
|
+
reasons.push(
|
|
485
|
+
`Row ${idx + 1} ("${row.name}") has neither uploaded ` +
|
|
486
|
+
`content nor a server document id`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return reasons.length ? invalidResult(undefined, reasons) : validResult();
|
|
491
|
+
}
|
|
492
|
+
case 'array-of-pairs': {
|
|
493
|
+
let valid = true;
|
|
494
|
+
const reasons: string[] = [];
|
|
495
|
+
// Check if every pair has key & value
|
|
496
|
+
// assigned properly
|
|
497
|
+
if (!Array.isArray(value)) {
|
|
498
|
+
return invalidResult('Value must be a list');
|
|
499
|
+
}
|
|
500
|
+
if (
|
|
501
|
+
!value?.every(
|
|
502
|
+
(pair: { [key: string]: string }): boolean =>
|
|
503
|
+
pair[field.fields[0]] !== '' && pair[field.fields[1]] !== ''
|
|
504
|
+
)
|
|
505
|
+
) {
|
|
506
|
+
valid = false;
|
|
507
|
+
reasons.push('All pairs must include both fields');
|
|
508
|
+
}
|
|
509
|
+
// Get a list of unique values
|
|
510
|
+
const uniqueValues: any[] = uniqWith(
|
|
511
|
+
value,
|
|
512
|
+
(cur, prev) => cur[field.fields[0]] === prev[field.fields[0]]
|
|
513
|
+
);
|
|
514
|
+
// Check if there are any duplicates
|
|
515
|
+
if (size(uniqueValues) !== size(value)) {
|
|
516
|
+
valid = false;
|
|
517
|
+
reasons.push('Pairs must use unique keys');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return valid ? validResult() : invalidResult(undefined, reasons);
|
|
521
|
+
}
|
|
522
|
+
case 'class-connectors': {
|
|
523
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
524
|
+
return invalidResult('At least one connector is required');
|
|
525
|
+
}
|
|
526
|
+
let valid = true;
|
|
527
|
+
const reasons: string[] = [];
|
|
528
|
+
// Check if every pair has name, input method and output method
|
|
529
|
+
// assigned properly
|
|
530
|
+
if (
|
|
531
|
+
!value?.every(
|
|
532
|
+
(pair: { [key: string]: string }): boolean =>
|
|
533
|
+
!!pair.name && pair.name !== '' && !!pair.method && pair.method !== ''
|
|
534
|
+
)
|
|
535
|
+
) {
|
|
536
|
+
valid = false;
|
|
537
|
+
reasons.push('Each connector must have a name and method');
|
|
538
|
+
}
|
|
539
|
+
// Get a list of unique values
|
|
540
|
+
const uniqueValues: any[] = uniqWith(value, (cur, prev) => cur.name === prev.name);
|
|
541
|
+
// Check if there are any duplicates
|
|
542
|
+
if (size(uniqueValues) !== size(value)) {
|
|
543
|
+
valid = false;
|
|
544
|
+
reasons.push('Connector names must be unique');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return valid ? validResult() : invalidResult(undefined, reasons);
|
|
548
|
+
}
|
|
549
|
+
// Classes check
|
|
550
|
+
case 'class-array': {
|
|
551
|
+
if (!(Array.isArray(value) && value.length > 0)) {
|
|
552
|
+
return invalidResult('At least one class must be provided');
|
|
553
|
+
}
|
|
554
|
+
let valid = true;
|
|
555
|
+
const reasons: string[] = [];
|
|
556
|
+
// Check if the fields are not empty
|
|
557
|
+
if (
|
|
558
|
+
!value?.every((pair: { [key: string]: string }): boolean => !!pair.name && pair.name !== '')
|
|
559
|
+
) {
|
|
560
|
+
valid = false;
|
|
561
|
+
reasons.push('Each class must have a name');
|
|
562
|
+
}
|
|
563
|
+
// Get a list of unique values
|
|
564
|
+
const uniqueValues: any[] = uniqWith(
|
|
565
|
+
value,
|
|
566
|
+
(cur, prev) => `${cur.prefix}${cur.name}` === `${prev.prefix}${prev.name}`
|
|
567
|
+
);
|
|
568
|
+
// Check if there are any duplicates
|
|
569
|
+
if (size(uniqueValues) !== size(value)) {
|
|
570
|
+
valid = false;
|
|
571
|
+
reasons.push('Class names must be unique');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return valid ? validResult() : invalidResult(undefined, reasons);
|
|
575
|
+
}
|
|
305
576
|
case 'number': {
|
|
306
577
|
return resultFromBoolean(
|
|
307
578
|
!isNaN(value) && (getTypeFromValue(value) === 'float' || getTypeFromValue(value) === 'int'),
|
|
@@ -318,15 +589,28 @@ export const _validateField = (
|
|
|
318
589
|
!isNaN(value) && (getTypeFromValue(value) === 'float' || getTypeFromValue(value) === 'int'),
|
|
319
590
|
'Value must be a float'
|
|
320
591
|
);
|
|
592
|
+
case 'select-array':
|
|
321
593
|
case 'multi-select':
|
|
322
594
|
case 'array':
|
|
323
595
|
case 'file-tree':
|
|
324
596
|
return resultFromBoolean(value && value.length > 0, 'At least one value is required');
|
|
325
|
-
case 'cron':
|
|
597
|
+
case 'cron': {
|
|
326
598
|
if (!value) {
|
|
327
599
|
return invalidResult('Cron value is required');
|
|
328
600
|
}
|
|
329
|
-
|
|
601
|
+
const expression = cronExpressionFromValue(value);
|
|
602
|
+
// A value that is neither a cron string nor a schedule hash is INVALID,
|
|
603
|
+
// not a crash: `isValidCron` calls `.trim()` on whatever it is given, so
|
|
604
|
+
// passing it a hash threw a TypeError out of validation and took the
|
|
605
|
+
// whole form down through the host's error boundary.
|
|
606
|
+
if (expression === undefined) {
|
|
607
|
+
return invalidResult('Cron expression is invalid');
|
|
608
|
+
}
|
|
609
|
+
return resultFromBoolean(
|
|
610
|
+
isValidCron(expression, { alias: true }),
|
|
611
|
+
'Cron expression is invalid'
|
|
612
|
+
);
|
|
613
|
+
}
|
|
330
614
|
case 'date':
|
|
331
615
|
return resultFromBoolean(
|
|
332
616
|
value !== undefined &&
|
|
@@ -396,7 +680,13 @@ export const _validateField = (
|
|
|
396
680
|
for (let i = 0; i < parsedValue.length; i++) {
|
|
397
681
|
const itemResult = validateFieldWithResult(
|
|
398
682
|
(field.ui_element_type as string) || (field.element_type as string),
|
|
399
|
-
|
|
683
|
+
// An element arrives bare OR in a `{value, type}` envelope: the
|
|
684
|
+
// envelope is what the editor writes, the bare form is what storage
|
|
685
|
+
// holds. Reaching straight for `.value` failed every stored element
|
|
686
|
+
// as empty, so a reloaded alert rule reported its delivery action
|
|
687
|
+
// invalid while showing all six options set -- and threw outright on
|
|
688
|
+
// a null element.
|
|
689
|
+
getListElementValue(parsedValue[i]),
|
|
400
690
|
field
|
|
401
691
|
);
|
|
402
692
|
|
|
@@ -408,18 +698,370 @@ export const _validateField = (
|
|
|
408
698
|
|
|
409
699
|
return validResult();
|
|
410
700
|
}
|
|
411
|
-
case '
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
701
|
+
case 'list-of-hashes': {
|
|
702
|
+
const parsedValue: any = maybeParseYaml(value);
|
|
703
|
+
|
|
704
|
+
if (!parsedValue || !isArray(parsedValue)) {
|
|
705
|
+
return invalidResult('Value must be a list of hashes');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
for (let i = 0; i < parsedValue.length; i++) {
|
|
709
|
+
const item = parsedValue[i];
|
|
710
|
+
|
|
711
|
+
if (!size(item)) {
|
|
712
|
+
return invalidResult(`Hash at index ${i} must not be empty`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const itemResult = validateFieldWithResult('hash', item);
|
|
716
|
+
|
|
717
|
+
if (!itemResult.isValid) {
|
|
718
|
+
return withContext(itemResult, `Hash at index ${i} is invalid`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return validResult();
|
|
723
|
+
}
|
|
724
|
+
case 'mapper-code': {
|
|
725
|
+
if (!value) {
|
|
726
|
+
return invalidResult('Mapper code is required');
|
|
727
|
+
}
|
|
728
|
+
// Split the value
|
|
729
|
+
const [code, method] = value.split('::');
|
|
730
|
+
// Both fields need to be strings & filled
|
|
731
|
+
const codeResult = validateFieldWithResult('string', code);
|
|
732
|
+
|
|
733
|
+
if (!codeResult.isValid) {
|
|
734
|
+
return withContext(codeResult, 'Mapper code is invalid');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const methodResult = validateFieldWithResult('string', method);
|
|
738
|
+
|
|
739
|
+
if (!methodResult.isValid) {
|
|
740
|
+
return withContext(methodResult, 'Mapper method is invalid');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return validResult();
|
|
744
|
+
}
|
|
745
|
+
case 'var-action': {
|
|
746
|
+
const varAction: TVariableActionValue = value;
|
|
747
|
+
|
|
748
|
+
if (
|
|
749
|
+
varAction?.var_type !== 'localvar' &&
|
|
750
|
+
varAction?.var_type !== 'globalvar' &&
|
|
751
|
+
varAction?.var_type !== 'autovar'
|
|
752
|
+
) {
|
|
753
|
+
return invalidResult('Variable type is invalid');
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const nameResult = validateFieldWithResult('string', varAction.var_name, {
|
|
757
|
+
has_to_have_value: true,
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
if (!nameResult.isValid) {
|
|
761
|
+
return withContext(nameResult, 'Variable name is invalid');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const actionResult = validateFieldWithResult('string', varAction.action_type, {
|
|
765
|
+
has_to_have_value: true,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (!actionResult.isValid) {
|
|
769
|
+
return withContext(actionResult, 'Variable action type is invalid');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// If the action type is transaction, the transaction_action needs to be set
|
|
773
|
+
if (varAction.action_type === 'transaction') {
|
|
774
|
+
const transactionResult = validateFieldWithResult('string', varAction.transaction_action, {
|
|
775
|
+
has_to_have_value: true,
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
if (!transactionResult.isValid) {
|
|
779
|
+
return withContext(transactionResult, 'Transaction action is invalid');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return validResult();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// If the variable data is missing
|
|
786
|
+
if (!field?.variableData?.value) {
|
|
787
|
+
return invalidResult('Variable data is missing');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Get the variable data
|
|
791
|
+
const variableData: TVariableActionValue = {
|
|
792
|
+
...value,
|
|
793
|
+
...field.variableData.value,
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
return withContext(
|
|
797
|
+
validateFieldWithResult(varAction.action_type, variableData, field),
|
|
798
|
+
'Variable action is invalid'
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
case 'type-selector':
|
|
802
|
+
case 'data-provider':
|
|
803
|
+
case 'api-call':
|
|
804
|
+
case 'search-single':
|
|
805
|
+
case 'send-message':
|
|
806
|
+
case 'search':
|
|
807
|
+
case 'update':
|
|
808
|
+
case 'delete':
|
|
809
|
+
case 'create': {
|
|
810
|
+
const newValue: IProviderType | null = maybeBuildOptionProvider(value);
|
|
811
|
+
|
|
812
|
+
if (!newValue) {
|
|
813
|
+
return invalidResult('Provider value is invalid');
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Api call only supports requests / response
|
|
817
|
+
if (type === 'api-call' && !value.supports_request) {
|
|
818
|
+
return invalidResult('API call must support requests');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// If the provider is from FSM variables, it needs pass this
|
|
822
|
+
if (
|
|
823
|
+
field?.isVariable &&
|
|
824
|
+
!(
|
|
825
|
+
newValue.supports_read ||
|
|
826
|
+
newValue.supports_create ||
|
|
827
|
+
newValue.supports_update ||
|
|
828
|
+
newValue.supports_delete ||
|
|
829
|
+
newValue.supports_request ||
|
|
830
|
+
newValue.supports_messages ||
|
|
831
|
+
newValue.transaction_management
|
|
832
|
+
)
|
|
833
|
+
) {
|
|
834
|
+
return invalidResult('Variable provider must support at least one action');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Send message only supports messages
|
|
838
|
+
if (
|
|
839
|
+
type === 'send-message' &&
|
|
840
|
+
(!newValue.supports_messages || !newValue.message_id || !newValue.message)
|
|
841
|
+
) {
|
|
842
|
+
return invalidResult('Send message provider must include message id and content');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (newValue.message_id) {
|
|
846
|
+
const messageIdResult = validateFieldWithResult('string', newValue.message_id);
|
|
847
|
+
|
|
848
|
+
if (!messageIdResult.isValid) {
|
|
849
|
+
return withContext(messageIdResult, 'Message id is invalid');
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!newValue.message) {
|
|
853
|
+
return invalidResult('Message content is missing');
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const messageResult = validateFieldWithResult(
|
|
857
|
+
newValue.message.type,
|
|
858
|
+
newValue.message.value
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
if (!messageResult.isValid) {
|
|
862
|
+
return withContext(messageResult, 'Message content is invalid');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (newValue.use_args) {
|
|
867
|
+
if (newValue.args?.type !== 'nothing') {
|
|
868
|
+
if (!newValue.args) {
|
|
869
|
+
return invalidResult('Arguments are missing');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const argsResult = validateFieldWithResult(
|
|
873
|
+
newValue.args.type === 'hash' ? 'system-options' : newValue.args.type,
|
|
874
|
+
newValue.args.value
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
if (!argsResult.isValid) {
|
|
878
|
+
return withContext(argsResult, 'Arguments are invalid');
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
(type === 'search-single' || type === 'search') &&
|
|
885
|
+
size(newValue.search_args) !== 0 &&
|
|
886
|
+
!validateFieldWithResult('system-options-with-operators', newValue.search_args).isValid
|
|
887
|
+
) {
|
|
888
|
+
return invalidResult('Search arguments are invalid');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const isUpdateOrCreate = type === 'update' || type === 'create';
|
|
892
|
+
|
|
893
|
+
if (isUpdateOrCreate) {
|
|
894
|
+
const areNormalArgsInvalid =
|
|
895
|
+
`${type}_args` in newValue &&
|
|
896
|
+
(size(newValue[`${type}_args`]) === 0 ||
|
|
897
|
+
!validateFieldWithResult('system-options', newValue[`${type}_args`]).isValid);
|
|
898
|
+
|
|
899
|
+
const areFreeFormArgsInvalid =
|
|
900
|
+
`${type}_args_freeform` in newValue &&
|
|
901
|
+
(size(newValue[`${type}_args_freeform`]) === 0 ||
|
|
902
|
+
!validateFieldWithResult('list-of-hashes', newValue[`${type}_args_freeform`]).isValid);
|
|
903
|
+
|
|
904
|
+
if (`${type}_args` in newValue && areNormalArgsInvalid && areFreeFormArgsInvalid) {
|
|
905
|
+
return invalidResult('Update/create arguments are invalid');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (`${type}_args_freeform` in newValue && areFreeFormArgsInvalid && areNormalArgsInvalid) {
|
|
909
|
+
return invalidResult('Update/create arguments are invalid');
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (newValue?.type === 'factory') {
|
|
914
|
+
if (newValue.optionsChanged) {
|
|
915
|
+
return invalidResult('Factory options are not saved');
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
let options = true;
|
|
919
|
+
|
|
920
|
+
if (newValue.options) {
|
|
921
|
+
options = validateFieldWithResult('system-options', newValue.options).isValid;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Type path and name are required
|
|
925
|
+
return resultFromBoolean(
|
|
926
|
+
!!(newValue.type && newValue.name && options),
|
|
927
|
+
'Factory provider is incomplete'
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (newValue.record_requires_search_options && newValue.searchOptionsChanged) {
|
|
932
|
+
return invalidResult('Search options must be saved');
|
|
419
933
|
}
|
|
420
934
|
|
|
421
|
-
if (
|
|
422
|
-
|
|
935
|
+
if (
|
|
936
|
+
newValue.search_options &&
|
|
937
|
+
!validateFieldWithResult('system-options', newValue.search_options).isValid
|
|
938
|
+
) {
|
|
939
|
+
return invalidResult('Search options are invalid');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return resultFromBoolean(
|
|
943
|
+
!!(newValue.type && newValue.name),
|
|
944
|
+
'Provider type and name required'
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
case 'context-selector': {
|
|
948
|
+
if (isString(value)) {
|
|
949
|
+
const cont: string[] = value.split(':');
|
|
950
|
+
const contextResult = validateFieldWithResult('string', cont[0]);
|
|
951
|
+
|
|
952
|
+
if (!contextResult.isValid) {
|
|
953
|
+
return withContext(contextResult, 'Context selector prefix is invalid');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const nameResult = validateFieldWithResult('string', cont[1]);
|
|
957
|
+
|
|
958
|
+
if (!nameResult.isValid) {
|
|
959
|
+
return withContext(nameResult, 'Context selector name is invalid');
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return validResult();
|
|
963
|
+
}
|
|
964
|
+
return resultFromBoolean(
|
|
965
|
+
!!value?.iface_kind && !!value?.name,
|
|
966
|
+
'Context selector requires iface_kind and name'
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
case 'service-event': {
|
|
970
|
+
const typeResult = validateFieldWithResult('type-selector', value);
|
|
971
|
+
|
|
972
|
+
if (!typeResult.isValid) {
|
|
973
|
+
return withContext(typeResult, 'Service event type is invalid');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (
|
|
977
|
+
!size(value.handlers) ||
|
|
978
|
+
!every(
|
|
979
|
+
value.handlers,
|
|
980
|
+
(handler: any) => (handler.type === 'fsm' || handler.type === 'method') && handler.value
|
|
981
|
+
)
|
|
982
|
+
) {
|
|
983
|
+
return invalidResult('Service event handlers are invalid');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return validResult();
|
|
987
|
+
}
|
|
988
|
+
case 'service-events': {
|
|
989
|
+
if (!isArray(value) || !size(value)) {
|
|
990
|
+
return invalidResult('At least one service event is required');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (let i = 0; i < value.length; i++) {
|
|
994
|
+
const serviceEventResult = validateFieldWithResult('service-event', value[i]);
|
|
995
|
+
|
|
996
|
+
if (!serviceEventResult.isValid) {
|
|
997
|
+
return withContext(serviceEventResult, `Service event ${i} is invalid`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return validResult();
|
|
1002
|
+
}
|
|
1003
|
+
case 'service-webhook': {
|
|
1004
|
+
if (
|
|
1005
|
+
!validateFieldWithResult('string', value?.name).isValid ||
|
|
1006
|
+
!validateFieldWithResult('string', value?.['rest-method']).isValid ||
|
|
1007
|
+
!validateFieldWithResult('string', value?.auth).isValid
|
|
1008
|
+
) {
|
|
1009
|
+
return invalidResult('Webhook name, method and auth are required');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (value.handler) {
|
|
1013
|
+
if (value.handler.type !== 'fsm' && value.handler.type !== 'method') {
|
|
1014
|
+
return invalidResult('Webhook handler type is invalid');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const handlerResult = validateFieldWithResult('string', value.handler.value);
|
|
1018
|
+
|
|
1019
|
+
if (!handlerResult.isValid) {
|
|
1020
|
+
return withContext(handlerResult, 'Webhook handler value is invalid');
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return validResult();
|
|
1025
|
+
}
|
|
1026
|
+
case 'service-webhooks': {
|
|
1027
|
+
if (!isArray(value) || !size(value)) {
|
|
1028
|
+
return invalidResult('At least one webhook is required');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
for (let i = 0; i < value.length; i++) {
|
|
1032
|
+
const webhookResult = validateFieldWithResult('service-webhook', value[i]);
|
|
1033
|
+
|
|
1034
|
+
if (!webhookResult.isValid) {
|
|
1035
|
+
return withContext(webhookResult, `Service webhook ${i} is invalid`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return validResult();
|
|
1040
|
+
}
|
|
1041
|
+
case 'auto':
|
|
1042
|
+
case 'any': {
|
|
1043
|
+
// Only a STRING has YAML in it. An `auto` field's value is usually
|
|
1044
|
+
// already the structure it describes -- a hash the form's hash editor
|
|
1045
|
+
// wrote, a list from a list editor -- and `jsyaml.load()` stringifies
|
|
1046
|
+
// whatever it is handed first: an object becomes `"[object Object]"`,
|
|
1047
|
+
// which YAML then reads as the flow sequence `["object Object"]`. So the
|
|
1048
|
+
// auto-detected type came back `list`, the value was validated as a list,
|
|
1049
|
+
// and every hash stored in an `auto` field was reported *"Value must be a
|
|
1050
|
+
// list"* -- an invalid marker on a value that is fine. A list fared no
|
|
1051
|
+
// better: `[1, 2]` stringified to `"1,2"` and detected as a string.
|
|
1052
|
+
//
|
|
1053
|
+
// Every other branch that accepts either shape already parses through
|
|
1054
|
+
// `maybeParseYaml`, which passes a non-string straight through; this one
|
|
1055
|
+
// reached for `jsyaml` directly. It cannot use `maybeParseYaml` as-is
|
|
1056
|
+
// because that reports a YAML error as an absent value, and "not valid
|
|
1057
|
+
// YAML" and "empty" are different answers here.
|
|
1058
|
+
let parsedData: any = value;
|
|
1059
|
+
if (isString(value)) {
|
|
1060
|
+
try {
|
|
1061
|
+
parsedData = jsyaml.load(value);
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
return invalidResult('Value is not valid YAML');
|
|
1064
|
+
}
|
|
423
1065
|
}
|
|
424
1066
|
|
|
425
1067
|
if (parsedData) {
|
|
@@ -431,7 +1073,187 @@ export const _validateField = (
|
|
|
431
1073
|
|
|
432
1074
|
return invalidResult('Value is empty');
|
|
433
1075
|
}
|
|
434
|
-
case '
|
|
1076
|
+
case 'processor': {
|
|
1077
|
+
if (!value || !value['processor-input-type'] || !value['processor-output-type']) {
|
|
1078
|
+
return invalidResult('Processor input and output types are required');
|
|
1079
|
+
}
|
|
1080
|
+
// Validate the input and output types
|
|
1081
|
+
if (value?.['processor-input-type']) {
|
|
1082
|
+
const inputResult = validateFieldWithResult(
|
|
1083
|
+
'type-selector',
|
|
1084
|
+
value?.['processor-input-type']
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
if (!inputResult.isValid) {
|
|
1088
|
+
return withContext(inputResult, 'Processor input type is invalid');
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (value?.['processor-output-type']) {
|
|
1093
|
+
const outputResult = validateFieldWithResult(
|
|
1094
|
+
'type-selector',
|
|
1095
|
+
value?.['processor-output-type']
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
if (!outputResult.isValid) {
|
|
1099
|
+
return withContext(outputResult, 'Processor output type is invalid');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return validResult();
|
|
1104
|
+
}
|
|
1105
|
+
case 'processor-mappings': {
|
|
1106
|
+
// processor-mappings is an array of field mappings
|
|
1107
|
+
// Each mapping must have an outputPath (the target field being mapped)
|
|
1108
|
+
// and either an inputPath (source field) or options configured
|
|
1109
|
+
if (!isArray(value)) {
|
|
1110
|
+
return canBeNull ? validResult() : invalidResult('Processor mappings must be a list');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
for (let i = 0; i < value.length; i++) {
|
|
1114
|
+
const mapping = value[i];
|
|
1115
|
+
|
|
1116
|
+
if (!mapping || !isObject(mapping)) {
|
|
1117
|
+
return invalidResult(`Mapping at index ${i} is invalid`);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// outputPath is always required - it identifies which output field is being mapped
|
|
1121
|
+
const outputPathResult = validateFieldWithResult('string', mapping.outputPath);
|
|
1122
|
+
|
|
1123
|
+
if (!outputPathResult.isValid) {
|
|
1124
|
+
return withContext(outputPathResult, `Mapping ${i} output path is invalid`);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// A mapping must have either an inputPath or options (or both)
|
|
1128
|
+
const hasInputPath = mapping.inputPath && mapping.inputPath !== '';
|
|
1129
|
+
const hasOptions = mapping.options && size(mapping.options) > 0;
|
|
1130
|
+
|
|
1131
|
+
if (!hasInputPath && !hasOptions) {
|
|
1132
|
+
return invalidResult(
|
|
1133
|
+
`Mapping for "${mapping.outputPath}" must have an input path or options configured`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return validResult();
|
|
1139
|
+
}
|
|
1140
|
+
case 'tool-catalog': {
|
|
1141
|
+
// tool-catalog is a flat string[] of selectors; each selector is one of:
|
|
1142
|
+
// "*" — all tools from every source
|
|
1143
|
+
// "system:*" — all Qorus system tools
|
|
1144
|
+
// "system:<tool>" — a specific system tool
|
|
1145
|
+
// "<connection>" — all tools from a connection
|
|
1146
|
+
// "<connection>/<tool>" — a specific tool from a connection
|
|
1147
|
+
if (!isArray(value)) {
|
|
1148
|
+
return canBeNull ? validResult() : invalidResult('Tool catalog must be a list');
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
for (let i = 0; i < value.length; i++) {
|
|
1152
|
+
const selector = value[i];
|
|
1153
|
+
if (typeof selector !== 'string' || selector.length === 0) {
|
|
1154
|
+
return invalidResult(`Tool selector at index ${i} must be a non-empty string`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return validResult();
|
|
1159
|
+
}
|
|
1160
|
+
case 'fsm-list': {
|
|
1161
|
+
if (!isArray(value)) {
|
|
1162
|
+
return invalidResult('FSM list must be an array');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
for (let i = 0; i < value.length; i++) {
|
|
1166
|
+
const itemResult = validateFieldWithResult('string', value[i]?.name);
|
|
1167
|
+
|
|
1168
|
+
if (!itemResult.isValid) {
|
|
1169
|
+
return withContext(itemResult, `FSM entry ${i} name is invalid`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return validResult();
|
|
1174
|
+
}
|
|
1175
|
+
case 'api-manager': {
|
|
1176
|
+
if (!value) {
|
|
1177
|
+
return invalidResult('API manager value is missing');
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const factoryResult = validateFieldWithResult('string', value.factory);
|
|
1181
|
+
|
|
1182
|
+
if (!factoryResult.isValid) {
|
|
1183
|
+
return withContext(factoryResult, 'API manager factory is invalid');
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const endpointsResult = validateFieldWithResult('api-endpoints', value.endpoints);
|
|
1187
|
+
|
|
1188
|
+
if (!endpointsResult.isValid) {
|
|
1189
|
+
return withContext(endpointsResult, 'API manager endpoints are invalid');
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return validResult();
|
|
1193
|
+
}
|
|
1194
|
+
case 'api-endpoints': {
|
|
1195
|
+
if (!isArray(value) || !size(value)) {
|
|
1196
|
+
return invalidResult('At least one API endpoint is required');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
for (let i = 0; i < value.length; i++) {
|
|
1200
|
+
const endpointResult = validateFieldWithResult('string', value[i]?.value);
|
|
1201
|
+
|
|
1202
|
+
if (!endpointResult.isValid) {
|
|
1203
|
+
return withContext(endpointResult, `Endpoint ${i} value is invalid`);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const authorizationResult = validateFieldWithResult(
|
|
1207
|
+
'api-endpoint-authorization',
|
|
1208
|
+
value[i]?.authorization,
|
|
1209
|
+
undefined,
|
|
1210
|
+
true
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
if (!authorizationResult.isValid) {
|
|
1214
|
+
return withContext(authorizationResult, `Endpoint ${i} authorization is invalid`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return validResult();
|
|
1219
|
+
}
|
|
1220
|
+
case 'api-endpoint-authorization': {
|
|
1221
|
+
if (!value) {
|
|
1222
|
+
return canBeNull ? validResult() : invalidResult('Authorization override is missing');
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (typeof value !== 'object' || isArray(value)) {
|
|
1226
|
+
return invalidResult('Authorization override must be an object');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (value.mode !== undefined && !['merge', 'replace'].includes(value.mode)) {
|
|
1230
|
+
return invalidResult('Authorization mode must be merge or replace');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (value.allow_anonymous !== undefined && typeof value.allow_anonymous !== 'boolean') {
|
|
1234
|
+
return invalidResult('Authorization allow_anonymous must be a boolean');
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (value.roles !== undefined) {
|
|
1238
|
+
if (!isArray(value.roles)) {
|
|
1239
|
+
return invalidResult('Authorization roles must be a list');
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
for (let i = 0; i < value.roles.length; i++) {
|
|
1243
|
+
const roleResult = validateFieldWithResult('string', value.roles[i]);
|
|
1244
|
+
|
|
1245
|
+
if (!roleResult.isValid) {
|
|
1246
|
+
return withContext(roleResult, `Authorization role ${i} is invalid`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return validResult();
|
|
1252
|
+
}
|
|
1253
|
+
case 'options':
|
|
1254
|
+
case 'pipeline-options':
|
|
1255
|
+
case 'mapper-options':
|
|
1256
|
+
case 'system-options': {
|
|
435
1257
|
const getIsValid = (
|
|
436
1258
|
options?: TQorusForm,
|
|
437
1259
|
optionSchema: IQorusFormSchema = {}
|
|
@@ -444,47 +1266,51 @@ export const _validateField = (
|
|
|
444
1266
|
return validResult();
|
|
445
1267
|
}
|
|
446
1268
|
|
|
447
|
-
if
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
!validateOptionWithRequiredGroups(options, optionSchema, optionData.required_groups)
|
|
460
|
-
) {
|
|
461
|
-
return invalidResult(`Option ${option} requires a value`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
1269
|
+
// Check if all required options are resolved by the current values
|
|
1270
|
+
const [unresolvedRequiredOption] = getUnresolvedRequiredOptions(optionSchema, options);
|
|
1271
|
+
|
|
1272
|
+
if (unresolvedRequiredOption) {
|
|
1273
|
+
return unresolvedRequiredOption.validation
|
|
1274
|
+
? withContext(
|
|
1275
|
+
unresolvedRequiredOption.validation,
|
|
1276
|
+
`Option ${unresolvedRequiredOption.name} is invalid`
|
|
1277
|
+
)
|
|
1278
|
+
: invalidResult(
|
|
1279
|
+
getUnresolvedRequiredOptionReason(unresolvedRequiredOption, optionSchema)
|
|
1280
|
+
);
|
|
464
1281
|
}
|
|
465
1282
|
|
|
466
1283
|
for (const option of Object.keys(options)) {
|
|
1284
|
+
const optionData = options[option];
|
|
1285
|
+
const optionType = ((typeof optionData === 'object' ? optionData?.type : undefined) ||
|
|
1286
|
+
(optionSchema?.[option] as TQorusFormFieldSchema)?.ui_type ||
|
|
1287
|
+
(optionSchema?.[option] as TQorusFormFieldSchema)?.type) as string;
|
|
1288
|
+
const optionValue =
|
|
1289
|
+
optionData && typeof optionData === 'object' ? optionData.value : optionData;
|
|
1290
|
+
|
|
467
1291
|
if (
|
|
468
|
-
optionSchema?.[option]?.
|
|
469
|
-
!
|
|
1292
|
+
!(optionSchema?.[option] as TQorusFormFieldSchema)?.required &&
|
|
1293
|
+
!isValueDefined(optionType, optionValue)
|
|
470
1294
|
) {
|
|
471
|
-
|
|
1295
|
+
continue;
|
|
472
1296
|
}
|
|
473
1297
|
|
|
474
1298
|
if (
|
|
475
|
-
optionSchema?.[option]?.
|
|
476
|
-
!
|
|
477
|
-
|
|
1299
|
+
(optionSchema?.[option] as TQorusFormFieldSchema)?.depends_on &&
|
|
1300
|
+
!hasAllDependenciesFullfilled(
|
|
1301
|
+
(optionSchema[option] as TQorusFormFieldSchema).depends_on,
|
|
1302
|
+
options,
|
|
1303
|
+
optionSchema
|
|
1304
|
+
)
|
|
478
1305
|
) {
|
|
479
|
-
|
|
1306
|
+
return invalidResult(`Option ${option} dependencies are not fulfilled`);
|
|
480
1307
|
}
|
|
481
1308
|
|
|
482
|
-
const optionData = options[option];
|
|
483
1309
|
const optionResult =
|
|
484
1310
|
typeof optionData !== 'object'
|
|
485
1311
|
? validateFieldWithResult(getTypeFromValue(optionData), optionData)
|
|
486
1312
|
: validateFieldWithResult(optionData.type, optionData.value, {
|
|
487
|
-
...
|
|
1313
|
+
...(optionSchema?.[option] as any),
|
|
488
1314
|
optionSchema,
|
|
489
1315
|
options,
|
|
490
1316
|
isFunction: optionData.is_expression,
|
|
@@ -512,9 +1338,55 @@ export const _validateField = (
|
|
|
512
1338
|
|
|
513
1339
|
return getIsValid(value, field?.optionSchema);
|
|
514
1340
|
}
|
|
1341
|
+
case 'system-options-with-operators': {
|
|
1342
|
+
const isValid = (val: TQorusForm): IValidationResult => {
|
|
1343
|
+
if (!val || size(val) === 0) {
|
|
1344
|
+
if (canBeNull) {
|
|
1345
|
+
return validResult();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return invalidResult('Options with operators are required');
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
for (const option of Object.keys(val)) {
|
|
1352
|
+
const optionData = val[option];
|
|
1353
|
+
const optionResult =
|
|
1354
|
+
typeof optionData !== 'object'
|
|
1355
|
+
? validateFieldWithResult(getTypeFromValue(optionData), optionData)
|
|
1356
|
+
: validateFieldWithResult(optionData.type, optionData.value);
|
|
1357
|
+
|
|
1358
|
+
if (!optionResult.isValid) {
|
|
1359
|
+
return withContext(optionResult, `Option ${option} is invalid`);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (
|
|
1363
|
+
!optionData.op ||
|
|
1364
|
+
!fixOperatorValue(optionData.op).every(
|
|
1365
|
+
(operator) => validateFieldWithResult('string', operator).isValid
|
|
1366
|
+
)
|
|
1367
|
+
) {
|
|
1368
|
+
return invalidResult(`Operators for option ${option} are invalid`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return validResult();
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
if (isArray(value)) {
|
|
1376
|
+
for (let i = 0; i < value.length; i++) {
|
|
1377
|
+
const optionResult = isValid(value[i]);
|
|
1378
|
+
|
|
1379
|
+
if (!optionResult.isValid) {
|
|
1380
|
+
return withContext(optionResult, `Option set ${i} is invalid`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return validResult();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return isValid(value);
|
|
1388
|
+
}
|
|
515
1389
|
case 'byte-size': {
|
|
516
|
-
// Ported from qorus-ide validations (`sizeUnit` instead of the IDE's
|
|
517
|
-
// `size`, which would shadow the lodash import here).
|
|
518
1390
|
if (typeof value !== 'string') return invalidResult('Byte size must be a string');
|
|
519
1391
|
|
|
520
1392
|
const [bytes, sizeUnit] = splitByteSize(value);
|
|
@@ -544,7 +1416,8 @@ export const _validateField = (
|
|
|
544
1416
|
|
|
545
1417
|
return validResult();
|
|
546
1418
|
}
|
|
547
|
-
case 'url':
|
|
1419
|
+
case 'url':
|
|
1420
|
+
case 'uri': {
|
|
548
1421
|
const protocolResult = validateFieldWithResult('string', getProtocol(value));
|
|
549
1422
|
|
|
550
1423
|
if (!protocolResult.isValid) {
|
|
@@ -560,8 +1433,7 @@ export const _validateField = (
|
|
|
560
1433
|
return validResult();
|
|
561
1434
|
}
|
|
562
1435
|
case 'schema-definition': {
|
|
563
|
-
//
|
|
564
|
-
// minimal NEW_FIELD.md shape check so malformed definitions can't
|
|
1436
|
+
// The minimal NEW_FIELD.md shape check, so malformed definitions can't
|
|
565
1437
|
// silently save through FormEngine.
|
|
566
1438
|
if (!isObject(value)) {
|
|
567
1439
|
return invalidResult('Schema definition must be an object');
|
|
@@ -572,65 +1444,102 @@ export const _validateField = (
|
|
|
572
1444
|
return validResult();
|
|
573
1445
|
}
|
|
574
1446
|
case 'expression': {
|
|
575
|
-
// Ported from qorus-ide validations `'expression'` case. The catalogue
|
|
576
|
-
// is supplied via `field.expressions` (the ExpressionBuilder passes it),
|
|
577
|
-
// not a global store as in the IDE.
|
|
578
1447
|
const castedValue = value as {
|
|
579
1448
|
value?: { exp?: string; args?: any[] };
|
|
580
1449
|
is_expression?: boolean;
|
|
581
1450
|
type?: string;
|
|
582
1451
|
};
|
|
583
|
-
const expressions
|
|
1452
|
+
const expressions = readExpressionCatalogue(field);
|
|
584
1453
|
|
|
585
|
-
// Expressions
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1454
|
+
// Expressions validation is complicated and needs to follow the following rules
|
|
1455
|
+
// 1. If the expression is empty, it is invalid
|
|
1456
|
+
// 2. If the expression has a type, it means its a normal argument (sub expressions don't have type)
|
|
1457
|
+
// 3. If the expression has no type and `exp` is present, it is a sub expression and must be validated as such
|
|
1458
|
+
// 4. If there are no `args` we need to check if the expression actually has any required arguments or what is the `min_args` property
|
|
1459
|
+
|
|
1460
|
+
// 1. If the expression is empty
|
|
1461
|
+
//
|
|
1462
|
+
// Checked BEFORE the catalogue guard below, and deliberately: an
|
|
1463
|
+
// expression with no value, or with no operation chosen, is incomplete on
|
|
1464
|
+
// its own terms. Nothing in the catalogue can make it valid, so making
|
|
1465
|
+
// these two answers wait for a network fetch meant the same option
|
|
1466
|
+
// reported valid or invalid depending on whether that fetch had landed —
|
|
1467
|
+
// the field's own "show invalid only" filter listed a different set of
|
|
1468
|
+
// rows on a fast machine than on a slow one.
|
|
589
1469
|
if (!castedValue) {
|
|
590
1470
|
return invalidResult('Expression value is empty');
|
|
591
1471
|
}
|
|
1472
|
+
|
|
592
1473
|
if (!castedValue.value?.exp) {
|
|
593
1474
|
return invalidResult('Expression operation is required');
|
|
594
1475
|
}
|
|
595
|
-
|
|
1476
|
+
|
|
1477
|
+
// Everything past this point needs the expression's definition, which
|
|
1478
|
+
// only the catalogue can supply. Until it loads there is nothing to check
|
|
1479
|
+
// against, so an expression that names an operation is left alone rather
|
|
1480
|
+
// than reported against a catalogue we do not have.
|
|
1481
|
+
if (!size(expressions)) {
|
|
1482
|
+
return validResult();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Get the expression definition from the catalogue
|
|
1486
|
+
const expressionDefinition = expressions.find(
|
|
1487
|
+
(expr: any) => expr.name === castedValue.value?.exp
|
|
1488
|
+
);
|
|
1489
|
+
|
|
596
1490
|
if (!expressionDefinition) {
|
|
597
1491
|
return invalidResult(`Expression definition ${String(castedValue.value?.exp)} not found`);
|
|
598
1492
|
}
|
|
1493
|
+
|
|
1494
|
+
// If the expression has no required args, we can consider it valid
|
|
599
1495
|
const hasRequiredArgs = expressionDefinition.args?.some((arg: any) => arg.required);
|
|
1496
|
+
|
|
600
1497
|
if (!hasRequiredArgs && size(castedValue.value?.args || []) === 0) {
|
|
601
1498
|
return validResult();
|
|
602
1499
|
}
|
|
1500
|
+
|
|
1501
|
+
// If there are no args provided but the expression requires some (normal expressions require at least 1 arg
|
|
1502
|
+
// because we checked for required args earlier), or min_args is set
|
|
603
1503
|
if (size(castedValue.value?.args || []) < (expressionDefinition.min_args || 1)) {
|
|
604
1504
|
return invalidResult('Not enough arguments provided');
|
|
605
1505
|
}
|
|
1506
|
+
|
|
1507
|
+
// Now we need to validate each argument, either as a normal value or as a sub-expression
|
|
606
1508
|
const args = castedValue.value?.args ?? [];
|
|
1509
|
+
|
|
607
1510
|
for (let index = 0; index < args.length; index++) {
|
|
608
1511
|
const argValue: any = args[index];
|
|
609
1512
|
const argDefinition = expressionDefinition.varargs
|
|
610
1513
|
? expressionDefinition.args[0]
|
|
611
1514
|
: expressionDefinition.args[index];
|
|
1515
|
+
|
|
612
1516
|
if (!argValue?.value && !argDefinition?.required) {
|
|
613
1517
|
continue;
|
|
614
1518
|
}
|
|
1519
|
+
|
|
615
1520
|
if (argValue?.is_expression) {
|
|
616
1521
|
const result = validateFieldWithResult('expression', argValue, {
|
|
617
1522
|
expressions,
|
|
618
1523
|
has_to_have_value: argDefinition?.required,
|
|
619
|
-
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
620
1526
|
if (!result.isValid) {
|
|
621
1527
|
return withContext(
|
|
622
1528
|
result,
|
|
623
1529
|
`Sub-expression for argument ${index + 1} ("${argDefinition?.display_name}") is invalid`
|
|
624
1530
|
);
|
|
625
1531
|
}
|
|
1532
|
+
|
|
626
1533
|
continue;
|
|
627
1534
|
}
|
|
1535
|
+
|
|
628
1536
|
const result = validateFieldWithResult(argValue.type, argValue.value, {
|
|
629
1537
|
expressions,
|
|
630
1538
|
allowed_values: argDefinition?.allowed_values,
|
|
631
1539
|
element_allowed_values: argDefinition?.element_allowed_values,
|
|
632
1540
|
has_to_have_value: argDefinition?.required,
|
|
633
|
-
}
|
|
1541
|
+
});
|
|
1542
|
+
|
|
634
1543
|
if (!result.isValid) {
|
|
635
1544
|
return withContext(
|
|
636
1545
|
result,
|
|
@@ -638,6 +1547,7 @@ export const _validateField = (
|
|
|
638
1547
|
);
|
|
639
1548
|
}
|
|
640
1549
|
}
|
|
1550
|
+
|
|
641
1551
|
return validResult();
|
|
642
1552
|
}
|
|
643
1553
|
case 'nothing':
|
|
@@ -647,7 +1557,6 @@ export const _validateField = (
|
|
|
647
1557
|
}
|
|
648
1558
|
};
|
|
649
1559
|
|
|
650
|
-
// Memoized version
|
|
651
1560
|
/**
|
|
652
1561
|
* Read one arg out of a hash, whichever of the two legitimate shapes it is in.
|
|
653
1562
|
*
|
|
@@ -667,6 +1576,7 @@ export const _validateField = (
|
|
|
667
1576
|
export const readArgValue = (arg: any): any =>
|
|
668
1577
|
isObject(arg) && 'value' in (arg as object) ? (arg as { value: unknown }).value : arg;
|
|
669
1578
|
|
|
1579
|
+
// Memoized version
|
|
670
1580
|
export const validateFieldWithResult: (
|
|
671
1581
|
type: string,
|
|
672
1582
|
value?: any,
|
|
@@ -742,7 +1652,10 @@ export const getValueOrDefaultValue = (value: any, defaultValue: any, canBeNull?
|
|
|
742
1652
|
return undefined;
|
|
743
1653
|
};
|
|
744
1654
|
|
|
745
|
-
|
|
1655
|
+
/** Deliberately un-annotated: the inferred literal union is what callers assign
|
|
1656
|
+
* into their own narrower type aliases, and widening it to `string` breaks
|
|
1657
|
+
* them. */
|
|
1658
|
+
export const getTypeFromValue = (value: any) => {
|
|
746
1659
|
switch (true) {
|
|
747
1660
|
case isNull(value):
|
|
748
1661
|
case isUndefined(value):
|
|
@@ -775,7 +1688,8 @@ export const validateOptionWithRequiredGroups = (
|
|
|
775
1688
|
const optionsInGroups = getOptionsFromRequiredGroups(schema, groups);
|
|
776
1689
|
return optionsInGroups.some((option) =>
|
|
777
1690
|
isValueDefined(
|
|
778
|
-
(schema[option]
|
|
1691
|
+
((schema[option] as TQorusFormFieldSchema)?.ui_type as string) ||
|
|
1692
|
+
((schema[option] as TQorusFormFieldSchema)?.type as string),
|
|
779
1693
|
options?.[option]?.value
|
|
780
1694
|
)
|
|
781
1695
|
);
|
|
@@ -784,6 +1698,179 @@ export const validateOptionWithRequiredGroups = (
|
|
|
784
1698
|
return true;
|
|
785
1699
|
};
|
|
786
1700
|
|
|
1701
|
+
/** Why a required option is not satisfied by the current values. */
|
|
1702
|
+
export type TUnresolvedRequiredOptionReason =
|
|
1703
|
+
/** No value at all — neither typed, nor defaulted, nor preselected. */
|
|
1704
|
+
| 'missing'
|
|
1705
|
+
/** The option's dependencies are not fulfilled, so its value cannot be final yet. */
|
|
1706
|
+
| 'dependency'
|
|
1707
|
+
/** A value is present but does not validate against the option's type / schema. */
|
|
1708
|
+
| 'invalid';
|
|
1709
|
+
|
|
1710
|
+
export interface IUnresolvedRequiredOption {
|
|
1711
|
+
/** The option's key in the schema. */
|
|
1712
|
+
name: string;
|
|
1713
|
+
reason: TUnresolvedRequiredOptionReason;
|
|
1714
|
+
/** The failing validation, present only for the `invalid` reason. */
|
|
1715
|
+
validation?: IValidationResult;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export const getUnresolvedRequiredOptionReason = (
|
|
1719
|
+
{ name, reason }: IUnresolvedRequiredOption,
|
|
1720
|
+
optionSchema?: IQorusFormSchema
|
|
1721
|
+
): string => {
|
|
1722
|
+
switch (reason) {
|
|
1723
|
+
case 'dependency':
|
|
1724
|
+
return `Option ${name} dependencies are not fulfilled`;
|
|
1725
|
+
case 'invalid':
|
|
1726
|
+
return `Option ${name} is invalid`;
|
|
1727
|
+
default:
|
|
1728
|
+
return (optionSchema?.[name] as TQorusFormFieldSchema)?.required
|
|
1729
|
+
? `Option ${name} is required`
|
|
1730
|
+
: `Option ${name} requires a value`;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* List every required option that the given values do not resolve.
|
|
1736
|
+
*
|
|
1737
|
+
* `required` on an option schema means "the effective configuration must carry
|
|
1738
|
+
* this option" — NOT "the user has to type it in". An option whose schema
|
|
1739
|
+
* default or preselected value already resolves it needs no user input at all,
|
|
1740
|
+
* which is why callers pass values that have been through `fixOptions()`
|
|
1741
|
+
* (defaults, preselected values and rich-text default envelopes applied)
|
|
1742
|
+
* rather than the raw values the server returned.
|
|
1743
|
+
*
|
|
1744
|
+
* This is the single owner of the "which required options still need input"
|
|
1745
|
+
* question: `validateField('options', …)` uses it for form validity, and
|
|
1746
|
+
* qorus-ide's `ConnectionManagementModal` uses it to decide whether a
|
|
1747
|
+
* connection can be created — and its OAuth2 flow started — without showing a
|
|
1748
|
+
* form at all.
|
|
1749
|
+
*/
|
|
1750
|
+
export const getUnresolvedRequiredOptions = (
|
|
1751
|
+
optionSchema: IQorusFormSchema = {},
|
|
1752
|
+
options: TQorusForm = {}
|
|
1753
|
+
): IUnresolvedRequiredOption[] => {
|
|
1754
|
+
const unresolved: IUnresolvedRequiredOption[] = [];
|
|
1755
|
+
|
|
1756
|
+
for (const [name, schemaEntry] of Object.entries(optionSchema ?? {})) {
|
|
1757
|
+
const optionData = schemaEntry as TQorusFormFieldSchema;
|
|
1758
|
+
|
|
1759
|
+
if (!optionData?.required && !size(optionData?.required_groups)) {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const optionField = options?.[name];
|
|
1764
|
+
const optionValue =
|
|
1765
|
+
optionField && typeof optionField === 'object' ? optionField.value : optionField;
|
|
1766
|
+
const optionType = ((typeof optionField === 'object' ? optionField?.type : undefined) ||
|
|
1767
|
+
optionData.ui_type ||
|
|
1768
|
+
optionData.type) as string;
|
|
1769
|
+
|
|
1770
|
+
// A dependent option cannot be considered resolved while its dependencies
|
|
1771
|
+
// are not: the schema it will be validated against — and with it any
|
|
1772
|
+
// default the server would send — is not final yet.
|
|
1773
|
+
if (
|
|
1774
|
+
optionData.depends_on &&
|
|
1775
|
+
!hasAllDependenciesFullfilled(optionData.depends_on, options, optionSchema)
|
|
1776
|
+
) {
|
|
1777
|
+
unresolved.push({ name, reason: 'dependency' });
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (!isValueDefined(optionType, optionValue)) {
|
|
1782
|
+
// An option required only through a group is resolved by any sibling in
|
|
1783
|
+
// that group carrying a value.
|
|
1784
|
+
if (
|
|
1785
|
+
optionData.required ||
|
|
1786
|
+
!validateOptionWithRequiredGroups(options, optionSchema, optionData.required_groups)
|
|
1787
|
+
) {
|
|
1788
|
+
unresolved.push({ name, reason: 'missing' });
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const validation =
|
|
1795
|
+
typeof optionField !== 'object'
|
|
1796
|
+
? validateFieldWithResult(getTypeFromValue(optionField), optionField)
|
|
1797
|
+
: validateFieldWithResult(optionField.type, optionField.value, {
|
|
1798
|
+
...(optionData as any),
|
|
1799
|
+
optionSchema,
|
|
1800
|
+
options,
|
|
1801
|
+
isFunction: optionField.is_expression,
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
if (!validation.isValid) {
|
|
1805
|
+
unresolved.push({ name, reason: 'invalid', validation });
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return unresolved;
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Split a dependency entry into the sibling it names and how its value is compared.
|
|
1814
|
+
*
|
|
1815
|
+
* Three forms: `name` (the sibling must simply have a value), `name=value` (it must
|
|
1816
|
+
* have exactly that value) and `name!=value` (it must have a value, and not that
|
|
1817
|
+
* one). The negative form is what lets a field say it applies to every variant of a
|
|
1818
|
+
* record but one — spelling that as an `any-of` list of every other variant is both
|
|
1819
|
+
* unreadable in the "Depends on" lock and silently wrong the moment a variant is
|
|
1820
|
+
* added.
|
|
1821
|
+
*
|
|
1822
|
+
* `!=` deliberately requires the sibling to be answered: "not X" is a statement about
|
|
1823
|
+
* an answer, so an unanswered sibling does not satisfy it and the field stays locked.
|
|
1824
|
+
*/
|
|
1825
|
+
export const parseDependency = (
|
|
1826
|
+
dependency: string
|
|
1827
|
+
): { name: string; op?: '=' | '!='; value?: string } => {
|
|
1828
|
+
const neqIdx = dependency.indexOf('!=');
|
|
1829
|
+
if (neqIdx !== -1) {
|
|
1830
|
+
return {
|
|
1831
|
+
name: dependency.substring(0, neqIdx),
|
|
1832
|
+
op: '!=',
|
|
1833
|
+
value: dependency.substring(neqIdx + 2),
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const eqIdx = dependency.indexOf('=');
|
|
1838
|
+
if (eqIdx !== -1) {
|
|
1839
|
+
return {
|
|
1840
|
+
name: dependency.substring(0, eqIdx),
|
|
1841
|
+
op: '=',
|
|
1842
|
+
value: dependency.substring(eqIdx + 1),
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
return { name: dependency };
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
/** Whether one dependency entry holds against the current form values. */
|
|
1850
|
+
const isDependencyFulfilled = (
|
|
1851
|
+
dependency: string,
|
|
1852
|
+
options: TQorusForm,
|
|
1853
|
+
optionsSchema?: IQorusFormSchema
|
|
1854
|
+
): boolean => {
|
|
1855
|
+
const { name, op, value } = parseDependency(dependency);
|
|
1856
|
+
|
|
1857
|
+
if (op) {
|
|
1858
|
+
const optValue = options?.[name]?.value;
|
|
1859
|
+
if (optValue == null) {
|
|
1860
|
+
return false;
|
|
1861
|
+
}
|
|
1862
|
+
return op === '=' ? String(optValue) === value : String(optValue) !== value;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
return options?.[name]
|
|
1866
|
+
? validateField(options[name].type, options[name].value, {
|
|
1867
|
+
...(optionsSchema?.[name] as unknown as IFieldValidationProps),
|
|
1868
|
+
options,
|
|
1869
|
+
optionSchema: optionsSchema,
|
|
1870
|
+
})
|
|
1871
|
+
: true;
|
|
1872
|
+
};
|
|
1873
|
+
|
|
787
1874
|
export const hasAllDependenciesFullfilled = (
|
|
788
1875
|
dependencies: string[] | string[][],
|
|
789
1876
|
options: TQorusForm,
|
|
@@ -794,38 +1881,19 @@ export const hasAllDependenciesFullfilled = (
|
|
|
794
1881
|
}
|
|
795
1882
|
|
|
796
1883
|
return dependencies.every((dependency: string | string[]) => {
|
|
1884
|
+
// A nested list is an ANY. It used to look each entry up as a whole form-field
|
|
1885
|
+
// name, so a `name=value` entry inside one found no field and returned `true`
|
|
1886
|
+
// unconditionally — the any-of group was satisfied by anything at all, while
|
|
1887
|
+
// CompactRow's lock rendered those same entries as real comparisons. Both halves
|
|
1888
|
+
// now go through one parser, so the grammar cannot mean two things.
|
|
797
1889
|
if (isArray(dependency)) {
|
|
798
|
-
return (dependency as string[]).some((dep) =>
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
...(optionsSchema?.[dep] as unknown as IFieldValidationProps),
|
|
802
|
-
options,
|
|
803
|
-
optionSchema: optionsSchema,
|
|
804
|
-
})
|
|
805
|
-
: true;
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (isString(dependency)) {
|
|
810
|
-
const eqIdx = (dependency as string).indexOf('=');
|
|
811
|
-
if (eqIdx !== -1) {
|
|
812
|
-
const depName = (dependency as string).substring(0, eqIdx);
|
|
813
|
-
const depValue = (dependency as string).substring(eqIdx + 1);
|
|
814
|
-
const optValue = options?.[depName]?.value;
|
|
815
|
-
return optValue != null && String(optValue) === depValue;
|
|
816
|
-
}
|
|
1890
|
+
return (dependency as string[]).some((dep) =>
|
|
1891
|
+
isDependencyFulfilled(dep, options, optionsSchema)
|
|
1892
|
+
);
|
|
817
1893
|
}
|
|
818
1894
|
|
|
819
|
-
return
|
|
820
|
-
?
|
|
821
|
-
options[dependency as string].type,
|
|
822
|
-
options[dependency as string].value,
|
|
823
|
-
{
|
|
824
|
-
...(optionsSchema?.[dependency as string] as unknown as IFieldValidationProps),
|
|
825
|
-
options,
|
|
826
|
-
optionSchema: optionsSchema,
|
|
827
|
-
}
|
|
828
|
-
)
|
|
1895
|
+
return isString(dependency)
|
|
1896
|
+
? isDependencyFulfilled(dependency as string, options, optionsSchema)
|
|
829
1897
|
: true;
|
|
830
1898
|
});
|
|
831
1899
|
};
|