@makehq/forman-schema 1.4.1 → 1.6.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/README.md +1 -0
- package/dist/index.cjs +268 -19
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +268 -19
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,22 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
|
|
30
30
|
// src/utils.ts
|
|
31
31
|
var FORMAN_VISUAL_TYPES = ["banner", "markdown", "html", "separator"];
|
|
32
|
+
function isVisualType(type) {
|
|
33
|
+
return FORMAN_VISUAL_TYPES.includes(type);
|
|
34
|
+
}
|
|
35
|
+
var FORMAN_REFERENCE_TYPES = [
|
|
36
|
+
"account",
|
|
37
|
+
"hook",
|
|
38
|
+
"device",
|
|
39
|
+
"keychain",
|
|
40
|
+
"datastore",
|
|
41
|
+
"aiagent",
|
|
42
|
+
"udt",
|
|
43
|
+
"scenario"
|
|
44
|
+
];
|
|
45
|
+
function isReferenceType(type) {
|
|
46
|
+
return FORMAN_REFERENCE_TYPES.includes(type);
|
|
47
|
+
}
|
|
32
48
|
function noEmpty(text) {
|
|
33
49
|
return text?.trim() || void 0;
|
|
34
50
|
}
|
|
@@ -51,8 +67,10 @@ var API_ENDPOINTS = {
|
|
|
51
67
|
aiagent: "api://ai-agents/v1/agents",
|
|
52
68
|
datastore: "api://data-stores",
|
|
53
69
|
hook: "api://hooks/{{kind}}",
|
|
70
|
+
device: "api://devices",
|
|
54
71
|
keychain: "api://keys/{{kind}}",
|
|
55
|
-
udt: "api://data-structures"
|
|
72
|
+
udt: "api://data-structures",
|
|
73
|
+
scenario: "api://scenario-list"
|
|
56
74
|
};
|
|
57
75
|
function normalizeFormanFieldType(field) {
|
|
58
76
|
const [type, kind] = field.type.split(":");
|
|
@@ -71,6 +89,130 @@ function normalizeFormanFieldType(field) {
|
|
|
71
89
|
}
|
|
72
90
|
};
|
|
73
91
|
}
|
|
92
|
+
function buildRestoreStructure(items) {
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const { domain, path, state } = item;
|
|
96
|
+
if (!result[domain]) {
|
|
97
|
+
result[domain] = {};
|
|
98
|
+
}
|
|
99
|
+
let current = result[domain];
|
|
100
|
+
for (const [index, key] of path.entries()) {
|
|
101
|
+
const nextKey = path[index + 1];
|
|
102
|
+
const isLastElement = index === path.length - 1;
|
|
103
|
+
const nextIsNumber = typeof nextKey === "number";
|
|
104
|
+
if (Array.isArray(current)) {
|
|
105
|
+
if (typeof key !== "number") {
|
|
106
|
+
throw new Error("Invalid path");
|
|
107
|
+
}
|
|
108
|
+
if (!current[key]) {
|
|
109
|
+
current[key] = {};
|
|
110
|
+
}
|
|
111
|
+
if (nextIsNumber) {
|
|
112
|
+
if (!current[key].value) {
|
|
113
|
+
current[key].value = {
|
|
114
|
+
mode: "chose",
|
|
115
|
+
items: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (!current[key].value.items) {
|
|
119
|
+
current[key].value.items = [];
|
|
120
|
+
}
|
|
121
|
+
current = current[key].value.items;
|
|
122
|
+
} else if (isLastElement) {
|
|
123
|
+
Object.assign(current[key], state);
|
|
124
|
+
} else {
|
|
125
|
+
current = current[key];
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (typeof key !== "string") {
|
|
129
|
+
throw new Error("Invalid path");
|
|
130
|
+
}
|
|
131
|
+
if (!current[key]) {
|
|
132
|
+
current[key] = {};
|
|
133
|
+
}
|
|
134
|
+
if (isLastElement) {
|
|
135
|
+
Object.assign(current[key], state);
|
|
136
|
+
} else {
|
|
137
|
+
if (nextIsNumber) {
|
|
138
|
+
if (!current[key].items) {
|
|
139
|
+
current[key].items = [];
|
|
140
|
+
current[key].mode = "chose";
|
|
141
|
+
}
|
|
142
|
+
current = current[key].items;
|
|
143
|
+
} else {
|
|
144
|
+
if (!current[key].nested) {
|
|
145
|
+
current[key].nested = {};
|
|
146
|
+
}
|
|
147
|
+
current = current[key].nested;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
var IML_FILTER_ENTRY_TYPES = ["null", "boolean", "number", "string"];
|
|
156
|
+
var IML_UNARY_FILTER_OPERATORS = ["exist", "notexist"];
|
|
157
|
+
var IML_BINARY_FILTER_OPERATORS = [
|
|
158
|
+
"text:equal",
|
|
159
|
+
"text:equal:ci",
|
|
160
|
+
"text:notequal",
|
|
161
|
+
"text:notequal:ci",
|
|
162
|
+
"text:contain",
|
|
163
|
+
"text:contain:ci",
|
|
164
|
+
"text:notcontain",
|
|
165
|
+
"text:notcontain:ci",
|
|
166
|
+
"text:startwith",
|
|
167
|
+
"text:startwith:ci",
|
|
168
|
+
"text:notstartwith",
|
|
169
|
+
"text:notstartwith:ci",
|
|
170
|
+
"text:endwith",
|
|
171
|
+
"text:endwith:ci",
|
|
172
|
+
"text:notendwith",
|
|
173
|
+
"text:notendwith:ci",
|
|
174
|
+
"text:pattern",
|
|
175
|
+
"text:pattern:ci",
|
|
176
|
+
"text:notpattern",
|
|
177
|
+
"text:notpattern:ci",
|
|
178
|
+
"number:equal",
|
|
179
|
+
"number:notequal",
|
|
180
|
+
"number:greater",
|
|
181
|
+
"number:less",
|
|
182
|
+
"number:greaterorequal",
|
|
183
|
+
"number:lessorequal",
|
|
184
|
+
"date:equal",
|
|
185
|
+
"date:notequal",
|
|
186
|
+
"date:greater",
|
|
187
|
+
"date:less",
|
|
188
|
+
"date:greaterorequal",
|
|
189
|
+
"date:lessorequal",
|
|
190
|
+
"time:equal",
|
|
191
|
+
"time:notequal",
|
|
192
|
+
"time:greater",
|
|
193
|
+
"time:less",
|
|
194
|
+
"time:greaterorequal",
|
|
195
|
+
"time:lessorequal",
|
|
196
|
+
"semver:equal",
|
|
197
|
+
"semver:notequal",
|
|
198
|
+
"semver:greater",
|
|
199
|
+
"semver:less",
|
|
200
|
+
"semver:greaterorequal",
|
|
201
|
+
"semver:lessorequal",
|
|
202
|
+
"array:contain",
|
|
203
|
+
"array:contain:ci",
|
|
204
|
+
"array:notcontain",
|
|
205
|
+
"array:notcontain:ci",
|
|
206
|
+
"array:equal",
|
|
207
|
+
"array:notequal",
|
|
208
|
+
"array:greater",
|
|
209
|
+
"array:less",
|
|
210
|
+
"array:greaterorequal",
|
|
211
|
+
"array:lessorequal",
|
|
212
|
+
"boolean:equal",
|
|
213
|
+
"boolean:notequal"
|
|
214
|
+
];
|
|
215
|
+
var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
|
|
74
216
|
|
|
75
217
|
// src/forman.ts
|
|
76
218
|
var SchemaConversionError = class extends Error {
|
|
@@ -106,6 +248,7 @@ var FORMAN_TYPE_MAP = {
|
|
|
106
248
|
email: "string",
|
|
107
249
|
filename: "string",
|
|
108
250
|
file: "string",
|
|
251
|
+
filter: "array",
|
|
109
252
|
folder: "string",
|
|
110
253
|
hidden: void 0,
|
|
111
254
|
integer: "number",
|
|
@@ -172,6 +315,8 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
|
|
|
172
315
|
case "file":
|
|
173
316
|
case "folder":
|
|
174
317
|
return handleSelectType(normalizedField, result, context);
|
|
318
|
+
case "filter":
|
|
319
|
+
return handleFilterType(normalizedField, result, context);
|
|
175
320
|
default:
|
|
176
321
|
return handlePrimitiveType(normalizedField, result);
|
|
177
322
|
}
|
|
@@ -182,7 +327,7 @@ function handleCollectionType(field, result, context) {
|
|
|
182
327
|
required: []
|
|
183
328
|
});
|
|
184
329
|
function addField(subField, tail) {
|
|
185
|
-
if (
|
|
330
|
+
if (isVisualType(subField.type)) {
|
|
186
331
|
return;
|
|
187
332
|
}
|
|
188
333
|
if (!subField.name) return;
|
|
@@ -248,6 +393,41 @@ function handleArrayType(field, result, context) {
|
|
|
248
393
|
}
|
|
249
394
|
return result;
|
|
250
395
|
}
|
|
396
|
+
function handleFilterType(field, result, context) {
|
|
397
|
+
const filterItems = {
|
|
398
|
+
oneOf: [
|
|
399
|
+
{
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
a: { type: IML_FILTER_ENTRY_TYPES },
|
|
403
|
+
o: { enum: IML_UNARY_FILTER_OPERATORS }
|
|
404
|
+
},
|
|
405
|
+
required: ["a", "o"]
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {
|
|
410
|
+
a: { type: IML_FILTER_ENTRY_TYPES },
|
|
411
|
+
b: { type: IML_FILTER_ENTRY_TYPES },
|
|
412
|
+
o: { enum: IML_BINARY_FILTER_OPERATORS }
|
|
413
|
+
},
|
|
414
|
+
required: ["a", "b", "o"]
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
const logic = field.logic ?? "default";
|
|
419
|
+
result.items = ["and", "or"].includes(logic) ? filterItems : {
|
|
420
|
+
type: "array",
|
|
421
|
+
items: filterItems
|
|
422
|
+
};
|
|
423
|
+
Object.defineProperty(result, "x-filter", {
|
|
424
|
+
configurable: true,
|
|
425
|
+
enumerable: true,
|
|
426
|
+
writable: true,
|
|
427
|
+
value: logic
|
|
428
|
+
});
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
251
431
|
function handleSelectType(field, result, context) {
|
|
252
432
|
const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
253
433
|
const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
|
|
@@ -404,6 +584,7 @@ var FORMAN_TYPE_MAP2 = {
|
|
|
404
584
|
email: "string",
|
|
405
585
|
filename: "string",
|
|
406
586
|
file: "string",
|
|
587
|
+
filter: "array",
|
|
407
588
|
folder: "string",
|
|
408
589
|
hidden: void 0,
|
|
409
590
|
integer: "number",
|
|
@@ -427,6 +608,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
427
608
|
(acc, domain) => {
|
|
428
609
|
acc[domain] = {
|
|
429
610
|
seenFields: /* @__PURE__ */ new Set(),
|
|
611
|
+
fieldStates: [],
|
|
430
612
|
validateFields: (fields, context) => {
|
|
431
613
|
return validateFormanValue(
|
|
432
614
|
domains[domain].values,
|
|
@@ -484,11 +666,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
484
666
|
}
|
|
485
667
|
return {
|
|
486
668
|
valid: errors.length === 0,
|
|
487
|
-
errors
|
|
669
|
+
errors,
|
|
670
|
+
states: errors.length === 0 && options?.states ? buildRestoreStructure(
|
|
671
|
+
Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
|
|
672
|
+
) : void 0
|
|
488
673
|
};
|
|
489
674
|
}
|
|
490
675
|
async function validateFormanValue(value, field, context) {
|
|
491
|
-
if (
|
|
676
|
+
if (isVisualType(field.type)) {
|
|
492
677
|
return {
|
|
493
678
|
valid: true,
|
|
494
679
|
errors: []
|
|
@@ -528,6 +713,24 @@ async function validateFormanValue(value, field, context) {
|
|
|
528
713
|
]
|
|
529
714
|
};
|
|
530
715
|
}
|
|
716
|
+
if (containsIMLExpression(value)) {
|
|
717
|
+
if (normalizedField.mappable === false) {
|
|
718
|
+
return {
|
|
719
|
+
valid: false,
|
|
720
|
+
errors: [
|
|
721
|
+
{
|
|
722
|
+
domain: context.domain,
|
|
723
|
+
path: context.path.join("."),
|
|
724
|
+
message: "Value contains prohibited IML expression."
|
|
725
|
+
}
|
|
726
|
+
]
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
valid: true,
|
|
731
|
+
errors: []
|
|
732
|
+
};
|
|
733
|
+
}
|
|
531
734
|
switch (normalizedField.type) {
|
|
532
735
|
case "collection":
|
|
533
736
|
return handleCollectionType2(value, normalizedField, context);
|
|
@@ -536,13 +739,17 @@ async function validateFormanValue(value, field, context) {
|
|
|
536
739
|
case "select":
|
|
537
740
|
case "account":
|
|
538
741
|
case "hook":
|
|
742
|
+
case "device":
|
|
539
743
|
case "keychain":
|
|
540
744
|
case "datastore":
|
|
541
745
|
case "aiagent":
|
|
542
746
|
case "udt":
|
|
747
|
+
case "scenario":
|
|
543
748
|
case "file":
|
|
544
749
|
case "folder":
|
|
545
750
|
return handleSelectType2(value, normalizedField, context);
|
|
751
|
+
case "filter":
|
|
752
|
+
return handleFilterType2(value, normalizedField, context);
|
|
546
753
|
default:
|
|
547
754
|
return handlePrimitiveType2(value, normalizedField, context);
|
|
548
755
|
}
|
|
@@ -579,7 +786,7 @@ async function handleCollectionType2(value, field, context) {
|
|
|
579
786
|
};
|
|
580
787
|
}
|
|
581
788
|
}
|
|
582
|
-
if (
|
|
789
|
+
if (isVisualType(subField.type)) {
|
|
583
790
|
continue;
|
|
584
791
|
}
|
|
585
792
|
if (!subField.name) {
|
|
@@ -596,7 +803,7 @@ async function handleCollectionType2(value, field, context) {
|
|
|
596
803
|
path: [...path, subField.name],
|
|
597
804
|
validateNestedFields: async (fields, context2) => {
|
|
598
805
|
for (const subField2 of fields) {
|
|
599
|
-
if (
|
|
806
|
+
if (isVisualType(subField2.type)) {
|
|
600
807
|
continue;
|
|
601
808
|
}
|
|
602
809
|
if (!subField2.name) {
|
|
@@ -643,7 +850,7 @@ async function handleArrayType2(value, field, context) {
|
|
|
643
850
|
const result = await validateFormanValue(
|
|
644
851
|
item,
|
|
645
852
|
Array.isArray(field.spec) ? { name: index.toString(), type: "collection", spec: field.spec } : Object.assign({}, field.spec, { name: index.toString() }),
|
|
646
|
-
{ ...context, path: [...context.path, index
|
|
853
|
+
{ ...context, path: [...context.path, index] }
|
|
647
854
|
);
|
|
648
855
|
errors.push(...result.errors);
|
|
649
856
|
}
|
|
@@ -669,6 +876,42 @@ async function handleArrayType2(value, field, context) {
|
|
|
669
876
|
errors
|
|
670
877
|
};
|
|
671
878
|
}
|
|
879
|
+
async function handleFilterType2(value, field, context) {
|
|
880
|
+
const filterEntry = {
|
|
881
|
+
type: "collection",
|
|
882
|
+
spec: [
|
|
883
|
+
{
|
|
884
|
+
name: "a",
|
|
885
|
+
type: "any",
|
|
886
|
+
required: true
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "b",
|
|
890
|
+
type: "any"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "o",
|
|
894
|
+
type: "text",
|
|
895
|
+
validate: {
|
|
896
|
+
enum: IML_FILTER_OPERATORS
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
]
|
|
900
|
+
};
|
|
901
|
+
const inlineSchema = ["and", "or"].includes(field.logic ?? "default") ? {
|
|
902
|
+
name: field.name,
|
|
903
|
+
type: "array",
|
|
904
|
+
spec: filterEntry
|
|
905
|
+
} : {
|
|
906
|
+
name: field.name,
|
|
907
|
+
type: "array",
|
|
908
|
+
spec: {
|
|
909
|
+
type: "array",
|
|
910
|
+
spec: filterEntry
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
return handleArrayType2(value, inlineSchema, context);
|
|
914
|
+
}
|
|
672
915
|
async function handleSelectType2(value, field, context) {
|
|
673
916
|
const errors = [];
|
|
674
917
|
let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
@@ -747,6 +990,15 @@ async function handleSelectType2(value, field, context) {
|
|
|
747
990
|
]
|
|
748
991
|
};
|
|
749
992
|
}
|
|
993
|
+
if (item.label) {
|
|
994
|
+
context.roots[context.domain].fieldStates.push({
|
|
995
|
+
path: context.path,
|
|
996
|
+
state: {
|
|
997
|
+
mode: isReferenceType(field.type) ? void 0 : "chose",
|
|
998
|
+
label: item.label
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
750
1002
|
if (item.nested) nested = item.nested;
|
|
751
1003
|
}
|
|
752
1004
|
if (nested) {
|
|
@@ -815,18 +1067,6 @@ async function handleNestedFields(nested, value, field, context) {
|
|
|
815
1067
|
}
|
|
816
1068
|
async function handlePrimitiveType2(value, field, context) {
|
|
817
1069
|
const errors = [];
|
|
818
|
-
if (containsIMLExpression(value)) {
|
|
819
|
-
return {
|
|
820
|
-
valid: true,
|
|
821
|
-
errors
|
|
822
|
-
};
|
|
823
|
-
}
|
|
824
|
-
if (errors.length > 0) {
|
|
825
|
-
return {
|
|
826
|
-
valid: false,
|
|
827
|
-
errors
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
1070
|
if (field.validate) {
|
|
831
1071
|
if (typeof value === "string") {
|
|
832
1072
|
if (field.validate.pattern && !new RegExp(
|
|
@@ -916,6 +1156,15 @@ function toFormanSchema(field) {
|
|
|
916
1156
|
spec
|
|
917
1157
|
};
|
|
918
1158
|
case "array":
|
|
1159
|
+
const filterLogic = Object.getOwnPropertyDescriptor(field, "x-filter");
|
|
1160
|
+
if (filterLogic) {
|
|
1161
|
+
return {
|
|
1162
|
+
type: "filter",
|
|
1163
|
+
label: noEmpty(field.title),
|
|
1164
|
+
help: noEmpty(field.description),
|
|
1165
|
+
logic: filterLogic.value === "default" ? void 0 : filterLogic.value
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
919
1168
|
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
920
1169
|
const formanSchema = {
|
|
921
1170
|
type: "array",
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema';
|
|
|
3
3
|
/**
|
|
4
4
|
* Valid Forman Schema field types
|
|
5
5
|
*/
|
|
6
|
-
type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'keychain' | 'datastore' | 'udt' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
|
|
6
|
+
type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
|
|
7
7
|
/**
|
|
8
8
|
* Validation configuration for Forman Schema fields
|
|
9
9
|
*/
|
|
@@ -79,6 +79,8 @@ type FormanSchemaField = {
|
|
|
79
79
|
sort?: string;
|
|
80
80
|
/** Adds an extra button to the field which opens an extra form. When the form is submitted, a specified RPC is called and the result is set as a new value of the parameter. */
|
|
81
81
|
rpc?: FormanSchemaRPCButton;
|
|
82
|
+
/** Definition of boolean logic to apply (filter only) */
|
|
83
|
+
logic?: 'and' | 'or' | 'reverse';
|
|
82
84
|
} & Record<`x-${string}`, unknown>;
|
|
83
85
|
/**
|
|
84
86
|
* RPC button allows for dynamic value retrieval from an external source
|
|
@@ -163,10 +165,21 @@ type FormanValidationResult = {
|
|
|
163
165
|
/** Error message */
|
|
164
166
|
message: string;
|
|
165
167
|
}[];
|
|
168
|
+
/** States of fields grouped by domain */
|
|
169
|
+
states?: Record<string, FormanSchemaFieldState>;
|
|
170
|
+
};
|
|
171
|
+
type FormanSchemaFieldState = {
|
|
172
|
+
mode?: 'chose' | 'edit';
|
|
173
|
+
label?: string;
|
|
174
|
+
data?: Record<string, unknown>;
|
|
175
|
+
nested?: Record<string, FormanSchemaFieldState>;
|
|
176
|
+
items?: Record<string, FormanSchemaFieldState>[];
|
|
166
177
|
};
|
|
167
178
|
type FormanValidationOptions = {
|
|
168
179
|
/** Unknown fields are not allowed when strict is true */
|
|
169
180
|
strict?: boolean;
|
|
181
|
+
/** Whether to generate states for fields */
|
|
182
|
+
states?: boolean;
|
|
170
183
|
/** Remote resource resolver */
|
|
171
184
|
resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
|
|
172
185
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema';
|
|
|
3
3
|
/**
|
|
4
4
|
* Valid Forman Schema field types
|
|
5
5
|
*/
|
|
6
|
-
type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'keychain' | 'datastore' | 'udt' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
|
|
6
|
+
type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
|
|
7
7
|
/**
|
|
8
8
|
* Validation configuration for Forman Schema fields
|
|
9
9
|
*/
|
|
@@ -79,6 +79,8 @@ type FormanSchemaField = {
|
|
|
79
79
|
sort?: string;
|
|
80
80
|
/** Adds an extra button to the field which opens an extra form. When the form is submitted, a specified RPC is called and the result is set as a new value of the parameter. */
|
|
81
81
|
rpc?: FormanSchemaRPCButton;
|
|
82
|
+
/** Definition of boolean logic to apply (filter only) */
|
|
83
|
+
logic?: 'and' | 'or' | 'reverse';
|
|
82
84
|
} & Record<`x-${string}`, unknown>;
|
|
83
85
|
/**
|
|
84
86
|
* RPC button allows for dynamic value retrieval from an external source
|
|
@@ -163,10 +165,21 @@ type FormanValidationResult = {
|
|
|
163
165
|
/** Error message */
|
|
164
166
|
message: string;
|
|
165
167
|
}[];
|
|
168
|
+
/** States of fields grouped by domain */
|
|
169
|
+
states?: Record<string, FormanSchemaFieldState>;
|
|
170
|
+
};
|
|
171
|
+
type FormanSchemaFieldState = {
|
|
172
|
+
mode?: 'chose' | 'edit';
|
|
173
|
+
label?: string;
|
|
174
|
+
data?: Record<string, unknown>;
|
|
175
|
+
nested?: Record<string, FormanSchemaFieldState>;
|
|
176
|
+
items?: Record<string, FormanSchemaFieldState>[];
|
|
166
177
|
};
|
|
167
178
|
type FormanValidationOptions = {
|
|
168
179
|
/** Unknown fields are not allowed when strict is true */
|
|
169
180
|
strict?: boolean;
|
|
181
|
+
/** Whether to generate states for fields */
|
|
182
|
+
states?: boolean;
|
|
170
183
|
/** Remote resource resolver */
|
|
171
184
|
resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
|
|
172
185
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
2
|
var FORMAN_VISUAL_TYPES = ["banner", "markdown", "html", "separator"];
|
|
3
|
+
function isVisualType(type) {
|
|
4
|
+
return FORMAN_VISUAL_TYPES.includes(type);
|
|
5
|
+
}
|
|
6
|
+
var FORMAN_REFERENCE_TYPES = [
|
|
7
|
+
"account",
|
|
8
|
+
"hook",
|
|
9
|
+
"device",
|
|
10
|
+
"keychain",
|
|
11
|
+
"datastore",
|
|
12
|
+
"aiagent",
|
|
13
|
+
"udt",
|
|
14
|
+
"scenario"
|
|
15
|
+
];
|
|
16
|
+
function isReferenceType(type) {
|
|
17
|
+
return FORMAN_REFERENCE_TYPES.includes(type);
|
|
18
|
+
}
|
|
3
19
|
function noEmpty(text) {
|
|
4
20
|
return text?.trim() || void 0;
|
|
5
21
|
}
|
|
@@ -22,8 +38,10 @@ var API_ENDPOINTS = {
|
|
|
22
38
|
aiagent: "api://ai-agents/v1/agents",
|
|
23
39
|
datastore: "api://data-stores",
|
|
24
40
|
hook: "api://hooks/{{kind}}",
|
|
41
|
+
device: "api://devices",
|
|
25
42
|
keychain: "api://keys/{{kind}}",
|
|
26
|
-
udt: "api://data-structures"
|
|
43
|
+
udt: "api://data-structures",
|
|
44
|
+
scenario: "api://scenario-list"
|
|
27
45
|
};
|
|
28
46
|
function normalizeFormanFieldType(field) {
|
|
29
47
|
const [type, kind] = field.type.split(":");
|
|
@@ -42,6 +60,130 @@ function normalizeFormanFieldType(field) {
|
|
|
42
60
|
}
|
|
43
61
|
};
|
|
44
62
|
}
|
|
63
|
+
function buildRestoreStructure(items) {
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
const { domain, path, state } = item;
|
|
67
|
+
if (!result[domain]) {
|
|
68
|
+
result[domain] = {};
|
|
69
|
+
}
|
|
70
|
+
let current = result[domain];
|
|
71
|
+
for (const [index, key] of path.entries()) {
|
|
72
|
+
const nextKey = path[index + 1];
|
|
73
|
+
const isLastElement = index === path.length - 1;
|
|
74
|
+
const nextIsNumber = typeof nextKey === "number";
|
|
75
|
+
if (Array.isArray(current)) {
|
|
76
|
+
if (typeof key !== "number") {
|
|
77
|
+
throw new Error("Invalid path");
|
|
78
|
+
}
|
|
79
|
+
if (!current[key]) {
|
|
80
|
+
current[key] = {};
|
|
81
|
+
}
|
|
82
|
+
if (nextIsNumber) {
|
|
83
|
+
if (!current[key].value) {
|
|
84
|
+
current[key].value = {
|
|
85
|
+
mode: "chose",
|
|
86
|
+
items: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (!current[key].value.items) {
|
|
90
|
+
current[key].value.items = [];
|
|
91
|
+
}
|
|
92
|
+
current = current[key].value.items;
|
|
93
|
+
} else if (isLastElement) {
|
|
94
|
+
Object.assign(current[key], state);
|
|
95
|
+
} else {
|
|
96
|
+
current = current[key];
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
if (typeof key !== "string") {
|
|
100
|
+
throw new Error("Invalid path");
|
|
101
|
+
}
|
|
102
|
+
if (!current[key]) {
|
|
103
|
+
current[key] = {};
|
|
104
|
+
}
|
|
105
|
+
if (isLastElement) {
|
|
106
|
+
Object.assign(current[key], state);
|
|
107
|
+
} else {
|
|
108
|
+
if (nextIsNumber) {
|
|
109
|
+
if (!current[key].items) {
|
|
110
|
+
current[key].items = [];
|
|
111
|
+
current[key].mode = "chose";
|
|
112
|
+
}
|
|
113
|
+
current = current[key].items;
|
|
114
|
+
} else {
|
|
115
|
+
if (!current[key].nested) {
|
|
116
|
+
current[key].nested = {};
|
|
117
|
+
}
|
|
118
|
+
current = current[key].nested;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
var IML_FILTER_ENTRY_TYPES = ["null", "boolean", "number", "string"];
|
|
127
|
+
var IML_UNARY_FILTER_OPERATORS = ["exist", "notexist"];
|
|
128
|
+
var IML_BINARY_FILTER_OPERATORS = [
|
|
129
|
+
"text:equal",
|
|
130
|
+
"text:equal:ci",
|
|
131
|
+
"text:notequal",
|
|
132
|
+
"text:notequal:ci",
|
|
133
|
+
"text:contain",
|
|
134
|
+
"text:contain:ci",
|
|
135
|
+
"text:notcontain",
|
|
136
|
+
"text:notcontain:ci",
|
|
137
|
+
"text:startwith",
|
|
138
|
+
"text:startwith:ci",
|
|
139
|
+
"text:notstartwith",
|
|
140
|
+
"text:notstartwith:ci",
|
|
141
|
+
"text:endwith",
|
|
142
|
+
"text:endwith:ci",
|
|
143
|
+
"text:notendwith",
|
|
144
|
+
"text:notendwith:ci",
|
|
145
|
+
"text:pattern",
|
|
146
|
+
"text:pattern:ci",
|
|
147
|
+
"text:notpattern",
|
|
148
|
+
"text:notpattern:ci",
|
|
149
|
+
"number:equal",
|
|
150
|
+
"number:notequal",
|
|
151
|
+
"number:greater",
|
|
152
|
+
"number:less",
|
|
153
|
+
"number:greaterorequal",
|
|
154
|
+
"number:lessorequal",
|
|
155
|
+
"date:equal",
|
|
156
|
+
"date:notequal",
|
|
157
|
+
"date:greater",
|
|
158
|
+
"date:less",
|
|
159
|
+
"date:greaterorequal",
|
|
160
|
+
"date:lessorequal",
|
|
161
|
+
"time:equal",
|
|
162
|
+
"time:notequal",
|
|
163
|
+
"time:greater",
|
|
164
|
+
"time:less",
|
|
165
|
+
"time:greaterorequal",
|
|
166
|
+
"time:lessorequal",
|
|
167
|
+
"semver:equal",
|
|
168
|
+
"semver:notequal",
|
|
169
|
+
"semver:greater",
|
|
170
|
+
"semver:less",
|
|
171
|
+
"semver:greaterorequal",
|
|
172
|
+
"semver:lessorequal",
|
|
173
|
+
"array:contain",
|
|
174
|
+
"array:contain:ci",
|
|
175
|
+
"array:notcontain",
|
|
176
|
+
"array:notcontain:ci",
|
|
177
|
+
"array:equal",
|
|
178
|
+
"array:notequal",
|
|
179
|
+
"array:greater",
|
|
180
|
+
"array:less",
|
|
181
|
+
"array:greaterorequal",
|
|
182
|
+
"array:lessorequal",
|
|
183
|
+
"boolean:equal",
|
|
184
|
+
"boolean:notequal"
|
|
185
|
+
];
|
|
186
|
+
var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
|
|
45
187
|
|
|
46
188
|
// src/forman.ts
|
|
47
189
|
var SchemaConversionError = class extends Error {
|
|
@@ -77,6 +219,7 @@ var FORMAN_TYPE_MAP = {
|
|
|
77
219
|
email: "string",
|
|
78
220
|
filename: "string",
|
|
79
221
|
file: "string",
|
|
222
|
+
filter: "array",
|
|
80
223
|
folder: "string",
|
|
81
224
|
hidden: void 0,
|
|
82
225
|
integer: "number",
|
|
@@ -143,6 +286,8 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
|
|
|
143
286
|
case "file":
|
|
144
287
|
case "folder":
|
|
145
288
|
return handleSelectType(normalizedField, result, context);
|
|
289
|
+
case "filter":
|
|
290
|
+
return handleFilterType(normalizedField, result, context);
|
|
146
291
|
default:
|
|
147
292
|
return handlePrimitiveType(normalizedField, result);
|
|
148
293
|
}
|
|
@@ -153,7 +298,7 @@ function handleCollectionType(field, result, context) {
|
|
|
153
298
|
required: []
|
|
154
299
|
});
|
|
155
300
|
function addField(subField, tail) {
|
|
156
|
-
if (
|
|
301
|
+
if (isVisualType(subField.type)) {
|
|
157
302
|
return;
|
|
158
303
|
}
|
|
159
304
|
if (!subField.name) return;
|
|
@@ -219,6 +364,41 @@ function handleArrayType(field, result, context) {
|
|
|
219
364
|
}
|
|
220
365
|
return result;
|
|
221
366
|
}
|
|
367
|
+
function handleFilterType(field, result, context) {
|
|
368
|
+
const filterItems = {
|
|
369
|
+
oneOf: [
|
|
370
|
+
{
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
a: { type: IML_FILTER_ENTRY_TYPES },
|
|
374
|
+
o: { enum: IML_UNARY_FILTER_OPERATORS }
|
|
375
|
+
},
|
|
376
|
+
required: ["a", "o"]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
a: { type: IML_FILTER_ENTRY_TYPES },
|
|
382
|
+
b: { type: IML_FILTER_ENTRY_TYPES },
|
|
383
|
+
o: { enum: IML_BINARY_FILTER_OPERATORS }
|
|
384
|
+
},
|
|
385
|
+
required: ["a", "b", "o"]
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
const logic = field.logic ?? "default";
|
|
390
|
+
result.items = ["and", "or"].includes(logic) ? filterItems : {
|
|
391
|
+
type: "array",
|
|
392
|
+
items: filterItems
|
|
393
|
+
};
|
|
394
|
+
Object.defineProperty(result, "x-filter", {
|
|
395
|
+
configurable: true,
|
|
396
|
+
enumerable: true,
|
|
397
|
+
writable: true,
|
|
398
|
+
value: logic
|
|
399
|
+
});
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
222
402
|
function handleSelectType(field, result, context) {
|
|
223
403
|
const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
224
404
|
const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
|
|
@@ -375,6 +555,7 @@ var FORMAN_TYPE_MAP2 = {
|
|
|
375
555
|
email: "string",
|
|
376
556
|
filename: "string",
|
|
377
557
|
file: "string",
|
|
558
|
+
filter: "array",
|
|
378
559
|
folder: "string",
|
|
379
560
|
hidden: void 0,
|
|
380
561
|
integer: "number",
|
|
@@ -398,6 +579,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
398
579
|
(acc, domain) => {
|
|
399
580
|
acc[domain] = {
|
|
400
581
|
seenFields: /* @__PURE__ */ new Set(),
|
|
582
|
+
fieldStates: [],
|
|
401
583
|
validateFields: (fields, context) => {
|
|
402
584
|
return validateFormanValue(
|
|
403
585
|
domains[domain].values,
|
|
@@ -455,11 +637,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
455
637
|
}
|
|
456
638
|
return {
|
|
457
639
|
valid: errors.length === 0,
|
|
458
|
-
errors
|
|
640
|
+
errors,
|
|
641
|
+
states: errors.length === 0 && options?.states ? buildRestoreStructure(
|
|
642
|
+
Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
|
|
643
|
+
) : void 0
|
|
459
644
|
};
|
|
460
645
|
}
|
|
461
646
|
async function validateFormanValue(value, field, context) {
|
|
462
|
-
if (
|
|
647
|
+
if (isVisualType(field.type)) {
|
|
463
648
|
return {
|
|
464
649
|
valid: true,
|
|
465
650
|
errors: []
|
|
@@ -499,6 +684,24 @@ async function validateFormanValue(value, field, context) {
|
|
|
499
684
|
]
|
|
500
685
|
};
|
|
501
686
|
}
|
|
687
|
+
if (containsIMLExpression(value)) {
|
|
688
|
+
if (normalizedField.mappable === false) {
|
|
689
|
+
return {
|
|
690
|
+
valid: false,
|
|
691
|
+
errors: [
|
|
692
|
+
{
|
|
693
|
+
domain: context.domain,
|
|
694
|
+
path: context.path.join("."),
|
|
695
|
+
message: "Value contains prohibited IML expression."
|
|
696
|
+
}
|
|
697
|
+
]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
valid: true,
|
|
702
|
+
errors: []
|
|
703
|
+
};
|
|
704
|
+
}
|
|
502
705
|
switch (normalizedField.type) {
|
|
503
706
|
case "collection":
|
|
504
707
|
return handleCollectionType2(value, normalizedField, context);
|
|
@@ -507,13 +710,17 @@ async function validateFormanValue(value, field, context) {
|
|
|
507
710
|
case "select":
|
|
508
711
|
case "account":
|
|
509
712
|
case "hook":
|
|
713
|
+
case "device":
|
|
510
714
|
case "keychain":
|
|
511
715
|
case "datastore":
|
|
512
716
|
case "aiagent":
|
|
513
717
|
case "udt":
|
|
718
|
+
case "scenario":
|
|
514
719
|
case "file":
|
|
515
720
|
case "folder":
|
|
516
721
|
return handleSelectType2(value, normalizedField, context);
|
|
722
|
+
case "filter":
|
|
723
|
+
return handleFilterType2(value, normalizedField, context);
|
|
517
724
|
default:
|
|
518
725
|
return handlePrimitiveType2(value, normalizedField, context);
|
|
519
726
|
}
|
|
@@ -550,7 +757,7 @@ async function handleCollectionType2(value, field, context) {
|
|
|
550
757
|
};
|
|
551
758
|
}
|
|
552
759
|
}
|
|
553
|
-
if (
|
|
760
|
+
if (isVisualType(subField.type)) {
|
|
554
761
|
continue;
|
|
555
762
|
}
|
|
556
763
|
if (!subField.name) {
|
|
@@ -567,7 +774,7 @@ async function handleCollectionType2(value, field, context) {
|
|
|
567
774
|
path: [...path, subField.name],
|
|
568
775
|
validateNestedFields: async (fields, context2) => {
|
|
569
776
|
for (const subField2 of fields) {
|
|
570
|
-
if (
|
|
777
|
+
if (isVisualType(subField2.type)) {
|
|
571
778
|
continue;
|
|
572
779
|
}
|
|
573
780
|
if (!subField2.name) {
|
|
@@ -614,7 +821,7 @@ async function handleArrayType2(value, field, context) {
|
|
|
614
821
|
const result = await validateFormanValue(
|
|
615
822
|
item,
|
|
616
823
|
Array.isArray(field.spec) ? { name: index.toString(), type: "collection", spec: field.spec } : Object.assign({}, field.spec, { name: index.toString() }),
|
|
617
|
-
{ ...context, path: [...context.path, index
|
|
824
|
+
{ ...context, path: [...context.path, index] }
|
|
618
825
|
);
|
|
619
826
|
errors.push(...result.errors);
|
|
620
827
|
}
|
|
@@ -640,6 +847,42 @@ async function handleArrayType2(value, field, context) {
|
|
|
640
847
|
errors
|
|
641
848
|
};
|
|
642
849
|
}
|
|
850
|
+
async function handleFilterType2(value, field, context) {
|
|
851
|
+
const filterEntry = {
|
|
852
|
+
type: "collection",
|
|
853
|
+
spec: [
|
|
854
|
+
{
|
|
855
|
+
name: "a",
|
|
856
|
+
type: "any",
|
|
857
|
+
required: true
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: "b",
|
|
861
|
+
type: "any"
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "o",
|
|
865
|
+
type: "text",
|
|
866
|
+
validate: {
|
|
867
|
+
enum: IML_FILTER_OPERATORS
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
]
|
|
871
|
+
};
|
|
872
|
+
const inlineSchema = ["and", "or"].includes(field.logic ?? "default") ? {
|
|
873
|
+
name: field.name,
|
|
874
|
+
type: "array",
|
|
875
|
+
spec: filterEntry
|
|
876
|
+
} : {
|
|
877
|
+
name: field.name,
|
|
878
|
+
type: "array",
|
|
879
|
+
spec: {
|
|
880
|
+
type: "array",
|
|
881
|
+
spec: filterEntry
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
return handleArrayType2(value, inlineSchema, context);
|
|
885
|
+
}
|
|
643
886
|
async function handleSelectType2(value, field, context) {
|
|
644
887
|
const errors = [];
|
|
645
888
|
let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
@@ -718,6 +961,15 @@ async function handleSelectType2(value, field, context) {
|
|
|
718
961
|
]
|
|
719
962
|
};
|
|
720
963
|
}
|
|
964
|
+
if (item.label) {
|
|
965
|
+
context.roots[context.domain].fieldStates.push({
|
|
966
|
+
path: context.path,
|
|
967
|
+
state: {
|
|
968
|
+
mode: isReferenceType(field.type) ? void 0 : "chose",
|
|
969
|
+
label: item.label
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
721
973
|
if (item.nested) nested = item.nested;
|
|
722
974
|
}
|
|
723
975
|
if (nested) {
|
|
@@ -786,18 +1038,6 @@ async function handleNestedFields(nested, value, field, context) {
|
|
|
786
1038
|
}
|
|
787
1039
|
async function handlePrimitiveType2(value, field, context) {
|
|
788
1040
|
const errors = [];
|
|
789
|
-
if (containsIMLExpression(value)) {
|
|
790
|
-
return {
|
|
791
|
-
valid: true,
|
|
792
|
-
errors
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
if (errors.length > 0) {
|
|
796
|
-
return {
|
|
797
|
-
valid: false,
|
|
798
|
-
errors
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
1041
|
if (field.validate) {
|
|
802
1042
|
if (typeof value === "string") {
|
|
803
1043
|
if (field.validate.pattern && !new RegExp(
|
|
@@ -887,6 +1127,15 @@ function toFormanSchema(field) {
|
|
|
887
1127
|
spec
|
|
888
1128
|
};
|
|
889
1129
|
case "array":
|
|
1130
|
+
const filterLogic = Object.getOwnPropertyDescriptor(field, "x-filter");
|
|
1131
|
+
if (filterLogic) {
|
|
1132
|
+
return {
|
|
1133
|
+
type: "filter",
|
|
1134
|
+
label: noEmpty(field.title),
|
|
1135
|
+
help: noEmpty(field.description),
|
|
1136
|
+
logic: filterLogic.value === "default" ? void 0 : filterLogic.value
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
890
1139
|
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
891
1140
|
const formanSchema = {
|
|
892
1141
|
type: "array",
|