@astroapps/forms-core 1.0.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/TODO.txt +3 -0
- package/lib/controlDefinition.d.ts +436 -0
- package/lib/defaultSchemaInterface.d.ts +28 -0
- package/lib/entityExpression.d.ts +33 -0
- package/lib/evalExpression.d.ts +15 -0
- package/lib/formNode.d.ts +45 -0
- package/lib/formState.d.ts +44 -0
- package/lib/index.cjs +1813 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +1521 -0
- package/lib/index.js.map +1 -0
- package/lib/schemaBuilder.d.ts +67 -0
- package/lib/schemaDataNode.d.ts +36 -0
- package/lib/schemaField.d.ts +122 -0
- package/lib/schemaInterface.d.ts +102 -0
- package/lib/schemaNode.d.ts +54 -0
- package/lib/schemaValidator.d.ts +27 -0
- package/lib/util.d.ts +14 -0
- package/lib/validators.d.ts +23 -0
- package/package.json +60 -0
- package/src/controlDefinition.ts +704 -0
- package/src/defaultSchemaInterface.ts +201 -0
- package/src/entityExpression.ts +39 -0
- package/src/evalExpression.ts +118 -0
- package/src/formNode.ts +249 -0
- package/src/formState.ts +491 -0
- package/src/index.ts +12 -0
- package/src/schemaBuilder.ts +318 -0
- package/src/schemaDataNode.ts +188 -0
- package/src/schemaField.ts +155 -0
- package/src/schemaInterface.ts +135 -0
- package/src/schemaNode.ts +285 -0
- package/src/schemaValidator.ts +32 -0
- package/src/util.ts +50 -0
- package/src/validators.ts +220 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EqualityFunc,
|
|
3
|
+
FieldOption,
|
|
4
|
+
SchemaField,
|
|
5
|
+
ValidationMessageType,
|
|
6
|
+
} from "./schemaField";
|
|
7
|
+
import { SchemaDataNode } from "./schemaDataNode";
|
|
8
|
+
import { SchemaNode } from "./schemaNode";
|
|
9
|
+
import { Control, ControlSetup } from "@astroapps/controls";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface for schema-related operations.
|
|
13
|
+
*/
|
|
14
|
+
export interface SchemaInterface {
|
|
15
|
+
/**
|
|
16
|
+
* Checks if the value of a field is empty.
|
|
17
|
+
* @param field The schema field.
|
|
18
|
+
* @param value The value to check.
|
|
19
|
+
* @returns True if the value is empty, false otherwise.
|
|
20
|
+
*/
|
|
21
|
+
isEmptyValue(field: SchemaField, value: any): boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the text representation of a field's value.
|
|
25
|
+
* @param field The schema field.
|
|
26
|
+
* @param value The value to convert.
|
|
27
|
+
* @param element Indicates if the value is an element, optional.
|
|
28
|
+
* @param options The field options, optional.
|
|
29
|
+
* @returns The text representation of the value.
|
|
30
|
+
*/
|
|
31
|
+
textValue(
|
|
32
|
+
field: SchemaField,
|
|
33
|
+
value: any,
|
|
34
|
+
element?: boolean,
|
|
35
|
+
options?: FieldOption[],
|
|
36
|
+
): string | undefined;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the text representation of a field's value for a data node.
|
|
40
|
+
* @param dataNode
|
|
41
|
+
*/
|
|
42
|
+
textValueForData(dataNode: SchemaDataNode): string | undefined;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets the length of a control's value.
|
|
46
|
+
* @param field The schema field.
|
|
47
|
+
* @param control The control to check.
|
|
48
|
+
* @returns The length of the control's value.
|
|
49
|
+
*/
|
|
50
|
+
controlLength(field: SchemaField, control: Control<any>): number;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gets the length of a field's value.
|
|
54
|
+
* @param field The schema field.
|
|
55
|
+
* @param value The value to check.
|
|
56
|
+
* @returns The length of the value.
|
|
57
|
+
*/
|
|
58
|
+
valueLength(field: SchemaField, value: any): number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gets the data options for a schema data node.
|
|
62
|
+
* @param node The schema data node.
|
|
63
|
+
* @returns The data options.
|
|
64
|
+
*/
|
|
65
|
+
getDataOptions(node: SchemaDataNode): FieldOption[] | null | undefined;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets the node options for a schema node.
|
|
69
|
+
* @param node The schema node.
|
|
70
|
+
* @returns The node options.
|
|
71
|
+
*/
|
|
72
|
+
getNodeOptions(node: SchemaNode): FieldOption[] | null | undefined;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the options for a schema field.
|
|
76
|
+
* @param field The schema field.
|
|
77
|
+
* @returns The field options.
|
|
78
|
+
*/
|
|
79
|
+
getOptions(field: SchemaField): FieldOption[] | undefined | null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets the filter options for a schema data node and field.
|
|
83
|
+
* @param array The schema data node.
|
|
84
|
+
* @param field The schema node.
|
|
85
|
+
* @returns The filter options.
|
|
86
|
+
*/
|
|
87
|
+
getFilterOptions(
|
|
88
|
+
array: SchemaDataNode,
|
|
89
|
+
field: SchemaNode,
|
|
90
|
+
): FieldOption[] | undefined | null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parses a string value to milliseconds.
|
|
94
|
+
* @param field The schema field.
|
|
95
|
+
* @param v The string value to parse.
|
|
96
|
+
* @returns The parsed value in milliseconds.
|
|
97
|
+
*/
|
|
98
|
+
parseToMillis(field: SchemaField, v: string): number;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the validation message text for a field.
|
|
102
|
+
* @param field The schema field.
|
|
103
|
+
* @param messageType The type of validation message.
|
|
104
|
+
* @param actual The actual value.
|
|
105
|
+
* @param expected The expected value.
|
|
106
|
+
* @returns The validation message text.
|
|
107
|
+
*/
|
|
108
|
+
validationMessageText(
|
|
109
|
+
field: SchemaField,
|
|
110
|
+
messageType: ValidationMessageType,
|
|
111
|
+
actual: any,
|
|
112
|
+
expected: any,
|
|
113
|
+
): string;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compares two values of a field.
|
|
117
|
+
* @param field The schema field.
|
|
118
|
+
* @param v1 The first value.
|
|
119
|
+
* @param v2 The second value.
|
|
120
|
+
* @returns The comparison result.
|
|
121
|
+
*/
|
|
122
|
+
compareValue(field: SchemaField, v1: unknown, v2: unknown): number;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets the search text for a field's value.
|
|
126
|
+
* @param field The schema field.
|
|
127
|
+
* @param value The value to search.
|
|
128
|
+
* @returns The search text.
|
|
129
|
+
*/
|
|
130
|
+
searchText(field: SchemaField, value: any): string;
|
|
131
|
+
|
|
132
|
+
makeEqualityFunc(field: SchemaNode, element?: boolean): EqualityFunc;
|
|
133
|
+
|
|
134
|
+
makeControlSetup(field: SchemaNode, element?: boolean): ControlSetup<any>;
|
|
135
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CompoundField,
|
|
3
|
+
FieldType,
|
|
4
|
+
isCompoundField,
|
|
5
|
+
missingField,
|
|
6
|
+
SchemaField,
|
|
7
|
+
} from "./schemaField";
|
|
8
|
+
|
|
9
|
+
export interface SchemaTreeLookup {
|
|
10
|
+
getSchema(schemaId: string): SchemaNode | undefined;
|
|
11
|
+
|
|
12
|
+
getSchemaTree(schemaId: string, additional?: SchemaField[]): SchemaTree | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export abstract class SchemaTree {
|
|
16
|
+
abstract rootNode: SchemaNode;
|
|
17
|
+
|
|
18
|
+
abstract getSchemaTree(schemaId: string): SchemaTree | undefined;
|
|
19
|
+
|
|
20
|
+
createChildNode(parent: SchemaNode, field: SchemaField): SchemaNode {
|
|
21
|
+
return new SchemaNode(parent.id + "/" + field.field, field, this, parent);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getSchema(schemaId: string): SchemaNode | undefined {
|
|
25
|
+
return this.getSchemaTree(schemaId)?.rootNode;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class SchemaTreeImpl extends SchemaTree {
|
|
30
|
+
rootNode: SchemaNode;
|
|
31
|
+
|
|
32
|
+
getSchemaTree(schemaId: string): SchemaTree | undefined {
|
|
33
|
+
return this.lookup?.getSchemaTree(schemaId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
rootFields: SchemaField[],
|
|
38
|
+
private lookup?: SchemaTreeLookup,
|
|
39
|
+
) {
|
|
40
|
+
super();
|
|
41
|
+
this.rootNode = new SchemaNode(
|
|
42
|
+
"",
|
|
43
|
+
{
|
|
44
|
+
type: FieldType.Compound,
|
|
45
|
+
field: "",
|
|
46
|
+
children: rootFields,
|
|
47
|
+
} as CompoundField,
|
|
48
|
+
this,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createSchemaTree(
|
|
54
|
+
rootFields: SchemaField[],
|
|
55
|
+
lookup?: SchemaTreeLookup,
|
|
56
|
+
): SchemaTree {
|
|
57
|
+
return new SchemaTreeImpl(rootFields, lookup);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class SchemaNode {
|
|
61
|
+
public constructor(
|
|
62
|
+
public id: string,
|
|
63
|
+
public field: SchemaField,
|
|
64
|
+
public tree: SchemaTree,
|
|
65
|
+
public parent?: SchemaNode,
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
getSchema(schemaId: string): SchemaNode | undefined {
|
|
69
|
+
return this.tree.getSchema(schemaId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getUnresolvedFields(): SchemaField[] {
|
|
73
|
+
return isCompoundField(this.field) ? this.field.children : [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getResolvedParent(noRecurse?: boolean): SchemaNode | undefined {
|
|
77
|
+
const f = this.field;
|
|
78
|
+
if (!isCompoundField(f)) return undefined;
|
|
79
|
+
const parentNode = f.schemaRef
|
|
80
|
+
? this.tree.getSchema(f.schemaRef)
|
|
81
|
+
: !noRecurse && f.treeChildren
|
|
82
|
+
? this.parent?.getResolvedParent()
|
|
83
|
+
: undefined;
|
|
84
|
+
return parentNode ?? this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getResolvedFields(): SchemaField[] {
|
|
88
|
+
const resolvedParent = this.getResolvedParent();
|
|
89
|
+
return resolvedParent?.getUnresolvedFields() ?? [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getChildNodes(): SchemaNode[] {
|
|
93
|
+
const node = this;
|
|
94
|
+
return node.getResolvedFields().map((x) => node.createChildNode(x));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getChildField(field: string): SchemaField {
|
|
98
|
+
return (
|
|
99
|
+
this.getResolvedFields().find((x) => x.field === field) ??
|
|
100
|
+
missingField(field)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
createChildNode(field: SchemaField): SchemaNode {
|
|
105
|
+
return this.tree.createChildNode(this, field);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getChildNode(field: string): SchemaNode {
|
|
109
|
+
return this.createChildNode(this.getChildField(field));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveSchemaNode(
|
|
114
|
+
node: SchemaNode,
|
|
115
|
+
fieldSegment: string,
|
|
116
|
+
): SchemaNode | undefined {
|
|
117
|
+
if (fieldSegment == ".") return node;
|
|
118
|
+
if (fieldSegment == "..") return node.parent;
|
|
119
|
+
return node.getChildNode(fieldSegment);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createSchemaNode(
|
|
123
|
+
field: SchemaField,
|
|
124
|
+
lookup: SchemaTree,
|
|
125
|
+
parent: SchemaNode | undefined,
|
|
126
|
+
): SchemaNode {
|
|
127
|
+
return new SchemaNode(
|
|
128
|
+
parent ? parent.id + "/" + field.field : field.field,
|
|
129
|
+
field,
|
|
130
|
+
lookup,
|
|
131
|
+
parent,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createSchemaLookup<A extends Record<string, SchemaField[]>>(
|
|
136
|
+
schemaMap: A,
|
|
137
|
+
): {
|
|
138
|
+
getSchema(schemaId: keyof A): SchemaNode;
|
|
139
|
+
getSchemaTree(schemaId: keyof A, additional?: SchemaField[]): SchemaTree;
|
|
140
|
+
} {
|
|
141
|
+
const lookup = {
|
|
142
|
+
getSchemaTree,
|
|
143
|
+
getSchema,
|
|
144
|
+
};
|
|
145
|
+
return lookup;
|
|
146
|
+
|
|
147
|
+
function getSchema(schemaId: keyof A): SchemaNode {
|
|
148
|
+
return getSchemaTree(schemaId)!.rootNode;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getSchemaTree(
|
|
152
|
+
schemaId: keyof A,
|
|
153
|
+
additional?: SchemaField[],
|
|
154
|
+
): SchemaTree {
|
|
155
|
+
const fields = schemaMap[schemaId];
|
|
156
|
+
if (fields) {
|
|
157
|
+
return new SchemaTreeImpl(
|
|
158
|
+
additional ? [...fields, ...additional] : fields,
|
|
159
|
+
lookup,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return undefined!;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function schemaForFieldRef(
|
|
167
|
+
fieldRef: string | undefined,
|
|
168
|
+
schema: SchemaNode,
|
|
169
|
+
): SchemaNode {
|
|
170
|
+
return schemaForFieldPath(fieldRef?.split("/") ?? [], schema);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function traverseSchemaPath<A>(
|
|
174
|
+
fieldPath: string[],
|
|
175
|
+
schema: SchemaNode,
|
|
176
|
+
acc: A,
|
|
177
|
+
next: (acc: A, node: SchemaNode) => A,
|
|
178
|
+
): A {
|
|
179
|
+
let i = 0;
|
|
180
|
+
while (i < fieldPath.length) {
|
|
181
|
+
const nextField = fieldPath[i];
|
|
182
|
+
let childNode = resolveSchemaNode(schema, nextField);
|
|
183
|
+
if (!childNode) {
|
|
184
|
+
childNode = createSchemaNode(
|
|
185
|
+
missingField(nextField),
|
|
186
|
+
schema.tree,
|
|
187
|
+
schema,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
acc = next(acc, childNode);
|
|
191
|
+
schema = childNode;
|
|
192
|
+
i++;
|
|
193
|
+
}
|
|
194
|
+
return acc;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function traverseData(
|
|
198
|
+
fieldPath: string[],
|
|
199
|
+
root: SchemaNode,
|
|
200
|
+
data: { [k: string]: any },
|
|
201
|
+
): unknown {
|
|
202
|
+
return traverseSchemaPath(
|
|
203
|
+
fieldPath,
|
|
204
|
+
root,
|
|
205
|
+
data,
|
|
206
|
+
(acc, n) => acc?.[n.field.field] as any,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function schemaForFieldPath(
|
|
211
|
+
fieldPath: string[],
|
|
212
|
+
schema: SchemaNode,
|
|
213
|
+
): SchemaNode {
|
|
214
|
+
let i = 0;
|
|
215
|
+
while (i < fieldPath.length) {
|
|
216
|
+
const nextField = fieldPath[i];
|
|
217
|
+
let childNode = resolveSchemaNode(schema, nextField);
|
|
218
|
+
if (!childNode) {
|
|
219
|
+
childNode = createSchemaNode(
|
|
220
|
+
missingField(nextField),
|
|
221
|
+
schema.tree,
|
|
222
|
+
schema,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
schema = childNode;
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
return schema;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getSchemaNodePath(node: SchemaNode) {
|
|
232
|
+
const paths: string[] = [];
|
|
233
|
+
let curNode: SchemaNode | undefined = node;
|
|
234
|
+
while (curNode) {
|
|
235
|
+
paths.push(curNode.field.field);
|
|
236
|
+
curNode = curNode.parent;
|
|
237
|
+
}
|
|
238
|
+
return paths.reverse();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getSchemaNodePathString(node: SchemaNode) {
|
|
242
|
+
return getSchemaNodePath(node).join("/");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function isCompoundNode(node: SchemaNode) {
|
|
246
|
+
return isCompoundField(node.field);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns the relative path from a parent node to a child node.
|
|
251
|
+
* @param parent
|
|
252
|
+
* @param child
|
|
253
|
+
*/
|
|
254
|
+
export function relativePath(parent: SchemaNode, child: SchemaNode): string {
|
|
255
|
+
// return the path from child to parent
|
|
256
|
+
if (parent.id === child.id) return ".";
|
|
257
|
+
|
|
258
|
+
const parentPath = getSchemaNodePath(parent);
|
|
259
|
+
const childPath = getSchemaNodePath(child);
|
|
260
|
+
return relativeSegmentPath(parentPath, childPath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Returns the relative path from a parent node to a child node.
|
|
265
|
+
* @param parentPath
|
|
266
|
+
* @param childPath
|
|
267
|
+
*/
|
|
268
|
+
export function relativeSegmentPath(
|
|
269
|
+
parentPath: string[],
|
|
270
|
+
childPath: string[],
|
|
271
|
+
): string {
|
|
272
|
+
let i = 0;
|
|
273
|
+
while (
|
|
274
|
+
i < parentPath.length &&
|
|
275
|
+
i < childPath.length &&
|
|
276
|
+
parentPath[i] === childPath[i]
|
|
277
|
+
) {
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const upLevels = parentPath.length - i;
|
|
282
|
+
const downPath = childPath.slice(i).join("/");
|
|
283
|
+
|
|
284
|
+
return "../".repeat(upLevels) + downPath;
|
|
285
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export enum ValidatorType {
|
|
2
|
+
Jsonata = "Jsonata",
|
|
3
|
+
Date = "Date",
|
|
4
|
+
Length = "Length",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SchemaValidator {
|
|
8
|
+
type: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JsonataValidator extends SchemaValidator {
|
|
12
|
+
type: ValidatorType.Jsonata;
|
|
13
|
+
expression: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LengthValidator extends SchemaValidator {
|
|
17
|
+
type: ValidatorType.Length;
|
|
18
|
+
min?: number | null;
|
|
19
|
+
max?: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export enum DateComparison {
|
|
23
|
+
NotBefore = "NotBefore",
|
|
24
|
+
NotAfter = "NotAfter",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DateValidator extends SchemaValidator {
|
|
28
|
+
type: ValidatorType.Date;
|
|
29
|
+
comparison: DateComparison;
|
|
30
|
+
fixedDate?: string | null;
|
|
31
|
+
daysFromCurrent?: number | null;
|
|
32
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addDependent,
|
|
3
|
+
CleanupScope,
|
|
4
|
+
Control, ControlSetup,
|
|
5
|
+
newControl,
|
|
6
|
+
updateComputedValue
|
|
7
|
+
} from "@astroapps/controls";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type representing a JSON path, which can be a string or a number.
|
|
11
|
+
*/
|
|
12
|
+
export type JsonPath = string | number;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Converts a JSON path array to a string.
|
|
16
|
+
* @param jsonPath - The JSON path array to convert.
|
|
17
|
+
* @param customIndex - Optional function to customize the index format.
|
|
18
|
+
* @returns The JSON path string.
|
|
19
|
+
*/
|
|
20
|
+
export function jsonPathString(
|
|
21
|
+
jsonPath: JsonPath[],
|
|
22
|
+
customIndex?: (n: number) => string,
|
|
23
|
+
) {
|
|
24
|
+
let out = "";
|
|
25
|
+
jsonPath.forEach((v, i) => {
|
|
26
|
+
if (typeof v === "number") {
|
|
27
|
+
out += customIndex?.(v) ?? "[" + v + "]";
|
|
28
|
+
} else {
|
|
29
|
+
if (i > 0) out += ".";
|
|
30
|
+
out += v;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createScopedComputed<T>(
|
|
37
|
+
parent: CleanupScope,
|
|
38
|
+
compute: () => T,
|
|
39
|
+
): Control<T> {
|
|
40
|
+
const c = newControl<any>(undefined);
|
|
41
|
+
updateComputedValue(c, compute);
|
|
42
|
+
addDependent(parent, c);
|
|
43
|
+
return c.as();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createScoped<T>(parent: CleanupScope, value: T, setup?: ControlSetup<T>): Control<T> {
|
|
47
|
+
const c = newControl<T>(value, setup);
|
|
48
|
+
addDependent(parent, c);
|
|
49
|
+
return c;
|
|
50
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DateComparison,
|
|
3
|
+
DateValidator,
|
|
4
|
+
JsonataValidator,
|
|
5
|
+
LengthValidator,
|
|
6
|
+
SchemaValidator,
|
|
7
|
+
ValidatorType,
|
|
8
|
+
} from "./schemaValidator";
|
|
9
|
+
import { ControlDefinition, isDataControl } from "./controlDefinition";
|
|
10
|
+
import { SchemaDataNode } from "./schemaDataNode";
|
|
11
|
+
import {
|
|
12
|
+
Control,
|
|
13
|
+
ControlChange,
|
|
14
|
+
createCleanupScope,
|
|
15
|
+
createEffect,
|
|
16
|
+
trackControlChange,
|
|
17
|
+
} from "@astroapps/controls";
|
|
18
|
+
import { ValidationMessageType } from "./schemaField";
|
|
19
|
+
import { SchemaInterface } from "./schemaInterface";
|
|
20
|
+
|
|
21
|
+
import { FormContextOptions } from "./formState";
|
|
22
|
+
import { FormNode } from "./formNode";
|
|
23
|
+
import { jsonataEval } from "./evalExpression";
|
|
24
|
+
import { ExpressionType } from "./entityExpression";
|
|
25
|
+
import { createScopedComputed } from "./util";
|
|
26
|
+
|
|
27
|
+
export interface ValidationEvalContext {
|
|
28
|
+
addSync(validate: (value: unknown) => string | undefined | null): void;
|
|
29
|
+
addCleanup(cleanup: () => void): void;
|
|
30
|
+
validationEnabled: Control<boolean>;
|
|
31
|
+
parentData: SchemaDataNode;
|
|
32
|
+
data: SchemaDataNode;
|
|
33
|
+
schemaInterface: SchemaInterface;
|
|
34
|
+
formContext: Control<FormContextOptions>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ValidatorEval<T extends SchemaValidator> = (
|
|
38
|
+
validation: T,
|
|
39
|
+
context: ValidationEvalContext,
|
|
40
|
+
) => void;
|
|
41
|
+
|
|
42
|
+
export const jsonataValidator: ValidatorEval<JsonataValidator> = (
|
|
43
|
+
validation,
|
|
44
|
+
context,
|
|
45
|
+
) => {
|
|
46
|
+
const error = createScopedComputed(context, () => undefined);
|
|
47
|
+
jsonataEval(
|
|
48
|
+
{ type: ExpressionType.Jsonata, expression: validation.expression },
|
|
49
|
+
{
|
|
50
|
+
scope: error,
|
|
51
|
+
dataNode: context.parentData,
|
|
52
|
+
returnResult: (v) => {
|
|
53
|
+
trackControlChange(context.data.control, ControlChange.Validate);
|
|
54
|
+
console.log("Setting jsonata error", v);
|
|
55
|
+
context.data.control.setError("jsonata", v?.toString());
|
|
56
|
+
},
|
|
57
|
+
schemaInterface: context.schemaInterface,
|
|
58
|
+
variables: context.formContext.fields.variables,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const lengthValidator: ValidatorEval<LengthValidator> = (
|
|
64
|
+
lv,
|
|
65
|
+
context,
|
|
66
|
+
) => {
|
|
67
|
+
const { schemaInterface } = context;
|
|
68
|
+
context.addSync(() => {
|
|
69
|
+
const field = context.data.schema.field;
|
|
70
|
+
const control = context.data.control;
|
|
71
|
+
const len = schemaInterface.controlLength(field, control);
|
|
72
|
+
if (lv.min != null && len < lv.min) {
|
|
73
|
+
if (field?.collection) {
|
|
74
|
+
control.setValue((v) =>
|
|
75
|
+
Array.isArray(v)
|
|
76
|
+
? v.concat(Array.from({ length: lv.min! - v.length }))
|
|
77
|
+
: Array.from({ length: lv.min! }),
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
return schemaInterface.validationMessageText(
|
|
81
|
+
field,
|
|
82
|
+
ValidationMessageType.MinLength,
|
|
83
|
+
len,
|
|
84
|
+
lv.min,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
} else if (lv.max != null && len > lv.max) {
|
|
88
|
+
return schemaInterface.validationMessageText(
|
|
89
|
+
field,
|
|
90
|
+
ValidationMessageType.MaxLength,
|
|
91
|
+
len,
|
|
92
|
+
lv.max,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const dateValidator: ValidatorEval<DateValidator> = (dv, context) => {
|
|
100
|
+
const { schemaInterface } = context;
|
|
101
|
+
const field = context.data.schema.field;
|
|
102
|
+
let comparisonDate: number;
|
|
103
|
+
if (dv.fixedDate) {
|
|
104
|
+
comparisonDate = schemaInterface.parseToMillis(field, dv.fixedDate);
|
|
105
|
+
} else {
|
|
106
|
+
const nowDate = new Date();
|
|
107
|
+
comparisonDate = Date.UTC(
|
|
108
|
+
nowDate.getFullYear(),
|
|
109
|
+
nowDate.getMonth(),
|
|
110
|
+
nowDate.getDate(),
|
|
111
|
+
);
|
|
112
|
+
if (dv.daysFromCurrent) {
|
|
113
|
+
comparisonDate += dv.daysFromCurrent * 86400000;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
context.addSync((v) => {
|
|
118
|
+
if (v) {
|
|
119
|
+
const selDate = schemaInterface.parseToMillis(field, v as string);
|
|
120
|
+
const notAfter = dv.comparison === DateComparison.NotAfter;
|
|
121
|
+
if (notAfter ? selDate > comparisonDate : selDate < comparisonDate) {
|
|
122
|
+
return schemaInterface.validationMessageText(
|
|
123
|
+
field,
|
|
124
|
+
notAfter
|
|
125
|
+
? ValidationMessageType.NotAfterDate
|
|
126
|
+
: ValidationMessageType.NotBeforeDate,
|
|
127
|
+
selDate,
|
|
128
|
+
comparisonDate,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const defaultValidators: Record<string, ValidatorEval<any>> = {
|
|
137
|
+
[ValidatorType.Jsonata]: jsonataValidator,
|
|
138
|
+
[ValidatorType.Length]: lengthValidator,
|
|
139
|
+
[ValidatorType.Date]: dateValidator,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export function createValidators(
|
|
143
|
+
def: ControlDefinition,
|
|
144
|
+
context: ValidationEvalContext,
|
|
145
|
+
): void {
|
|
146
|
+
if (isDataControl(def)) {
|
|
147
|
+
const { schemaInterface } = context;
|
|
148
|
+
if (def.required) {
|
|
149
|
+
context.addSync((v) => {
|
|
150
|
+
const field = context.data.schema.field;
|
|
151
|
+
return schemaInterface.isEmptyValue(field, v)
|
|
152
|
+
? schemaInterface.validationMessageText(
|
|
153
|
+
field,
|
|
154
|
+
ValidationMessageType.NotEmpty,
|
|
155
|
+
false,
|
|
156
|
+
true,
|
|
157
|
+
)
|
|
158
|
+
: null;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
def.validators?.forEach((x) => defaultValidators[x.type]?.(x, context));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function setupValidation(
|
|
166
|
+
controlImpl: Control<FormContextOptions>,
|
|
167
|
+
definition: ControlDefinition,
|
|
168
|
+
dataNode: Control<SchemaDataNode | undefined>,
|
|
169
|
+
schemaInterface: SchemaInterface,
|
|
170
|
+
parent: SchemaDataNode,
|
|
171
|
+
formNode: FormNode,
|
|
172
|
+
) {
|
|
173
|
+
const validationEnabled = createScopedComputed(
|
|
174
|
+
controlImpl,
|
|
175
|
+
() => !definition.hidden,
|
|
176
|
+
);
|
|
177
|
+
const validatorsScope = createCleanupScope();
|
|
178
|
+
createEffect(
|
|
179
|
+
() => {
|
|
180
|
+
validatorsScope.cleanup();
|
|
181
|
+
const dn = dataNode.value;
|
|
182
|
+
if (dn) {
|
|
183
|
+
let syncValidations: ((v: unknown) => string | undefined | null)[] = [];
|
|
184
|
+
createValidators(formNode.definition, {
|
|
185
|
+
data: dn,
|
|
186
|
+
parentData: parent,
|
|
187
|
+
validationEnabled,
|
|
188
|
+
schemaInterface,
|
|
189
|
+
addSync(validate: (v: unknown) => string | undefined | null) {
|
|
190
|
+
syncValidations.push(validate);
|
|
191
|
+
},
|
|
192
|
+
addCleanup(cleanup: () => void) {
|
|
193
|
+
validatorsScope.addCleanup(cleanup);
|
|
194
|
+
},
|
|
195
|
+
formContext: controlImpl,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
createEffect(
|
|
199
|
+
() => {
|
|
200
|
+
if (!validationEnabled.value) return undefined;
|
|
201
|
+
trackControlChange(dn.control, ControlChange.Validate);
|
|
202
|
+
const value = dn.control.value;
|
|
203
|
+
let error = null;
|
|
204
|
+
for (const syncValidation of syncValidations) {
|
|
205
|
+
error = syncValidation(value);
|
|
206
|
+
if (error) break;
|
|
207
|
+
}
|
|
208
|
+
return error;
|
|
209
|
+
},
|
|
210
|
+
(e) => {
|
|
211
|
+
dn.control.setError("default", e);
|
|
212
|
+
},
|
|
213
|
+
validatorsScope,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
(c) => {},
|
|
218
|
+
controlImpl,
|
|
219
|
+
);
|
|
220
|
+
}
|