@makehq/forman-schema 1.7.1 → 1.8.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/dist/index.cjs +216 -10
- package/dist/index.d.cts +32 -5
- package/dist/index.d.ts +32 -5
- package/dist/index.js +216 -10
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -316,11 +316,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
|
|
|
316
316
|
case "aiagent":
|
|
317
317
|
case "file":
|
|
318
318
|
case "folder":
|
|
319
|
-
return
|
|
319
|
+
return handleSelectOrPathType(normalizedField, result, context);
|
|
320
320
|
case "filter":
|
|
321
321
|
return handleFilterType(normalizedField, result, context);
|
|
322
322
|
default:
|
|
323
|
-
return handlePrimitiveType(normalizedField, result);
|
|
323
|
+
return handlePrimitiveType(normalizedField, result, context);
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
function handleCollectionType(field, result, context) {
|
|
@@ -436,8 +436,22 @@ function handleFilterType(field, result, context) {
|
|
|
436
436
|
});
|
|
437
437
|
return result;
|
|
438
438
|
}
|
|
439
|
-
function
|
|
439
|
+
function handleSelectOrPathType(field, result, context) {
|
|
440
440
|
const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
441
|
+
if (["file", "folder"].includes(field.type)) {
|
|
442
|
+
const optionsWrapper = isObject(field.options) ? field.options : void 0;
|
|
443
|
+
Object.defineProperty(result, "x-path", {
|
|
444
|
+
configurable: true,
|
|
445
|
+
enumerable: true,
|
|
446
|
+
writable: true,
|
|
447
|
+
value: {
|
|
448
|
+
type: field.type,
|
|
449
|
+
showRoot: optionsWrapper?.showRoot ?? true,
|
|
450
|
+
singleLevel: optionsWrapper?.singleLevel ?? false,
|
|
451
|
+
ownName: field.name
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
441
455
|
const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
|
|
442
456
|
const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
|
|
443
457
|
const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
|
|
@@ -546,9 +560,10 @@ function handleSelectType(field, result, context) {
|
|
|
546
560
|
)
|
|
547
561
|
});
|
|
548
562
|
}
|
|
563
|
+
if (field.rpc) result = processRpcDirective(field, result, context);
|
|
549
564
|
return result;
|
|
550
565
|
}
|
|
551
|
-
function handlePrimitiveType(field, result) {
|
|
566
|
+
function handlePrimitiveType(field, result, context) {
|
|
552
567
|
if (field.default !== "" && field.default != null) {
|
|
553
568
|
result.default = field.default;
|
|
554
569
|
}
|
|
@@ -566,6 +581,21 @@ function handlePrimitiveType(field, result) {
|
|
|
566
581
|
result.enum = field.validate.enum;
|
|
567
582
|
}
|
|
568
583
|
}
|
|
584
|
+
if (field.rpc) result = processRpcDirective(field, result, context);
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
function processRpcDirective(field, result, context) {
|
|
588
|
+
if (!field.rpc) return result;
|
|
589
|
+
Object.defineProperty(result, "x-search", {
|
|
590
|
+
configurable: true,
|
|
591
|
+
enumerable: true,
|
|
592
|
+
writable: true,
|
|
593
|
+
value: {
|
|
594
|
+
url: appendQueryString(field.rpc.url, context.domain, context.tail),
|
|
595
|
+
label: field.rpc.label,
|
|
596
|
+
inputSchema: typeof field.rpc.parameters === "string" ? { $ref: field.rpc.parameters } : toJSONSchemaInternal({ type: "collection", spec: field.rpc.parameters }, context)
|
|
597
|
+
}
|
|
598
|
+
});
|
|
569
599
|
return result;
|
|
570
600
|
}
|
|
571
601
|
|
|
@@ -654,7 +684,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
654
684
|
validateNestedFields: () => {
|
|
655
685
|
throw new Error("Cannot validate nested fields without parent field.");
|
|
656
686
|
},
|
|
657
|
-
resolveRemote: async (path, context) => {
|
|
687
|
+
resolveRemote: async (path, context, localData) => {
|
|
658
688
|
if (!options?.resolveRemote) {
|
|
659
689
|
throw new Error("Remote resource not supported when resolver is not provided.");
|
|
660
690
|
}
|
|
@@ -665,7 +695,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
665
695
|
},
|
|
666
696
|
{}
|
|
667
697
|
);
|
|
668
|
-
return await options.resolveRemote(path,
|
|
698
|
+
return await options.resolveRemote(path, {
|
|
699
|
+
...data,
|
|
700
|
+
...localData
|
|
701
|
+
});
|
|
669
702
|
}
|
|
670
703
|
}
|
|
671
704
|
);
|
|
@@ -752,9 +785,10 @@ async function validateFormanValue(value, field, context) {
|
|
|
752
785
|
case "aiagent":
|
|
753
786
|
case "udt":
|
|
754
787
|
case "scenario":
|
|
788
|
+
return handleSelectType(value, normalizedField, context);
|
|
755
789
|
case "file":
|
|
756
790
|
case "folder":
|
|
757
|
-
return
|
|
791
|
+
return handlePathType(value, normalizedField, context);
|
|
758
792
|
case "filter":
|
|
759
793
|
return handleFilterType2(value, normalizedField, context);
|
|
760
794
|
default:
|
|
@@ -919,7 +953,152 @@ async function handleFilterType2(value, field, context) {
|
|
|
919
953
|
};
|
|
920
954
|
return handleArrayType2(value, inlineSchema, context);
|
|
921
955
|
}
|
|
922
|
-
async function
|
|
956
|
+
async function handlePathType(value, field, context) {
|
|
957
|
+
const errors = [];
|
|
958
|
+
if (typeof value !== "string") {
|
|
959
|
+
return {
|
|
960
|
+
valid: false,
|
|
961
|
+
errors: [
|
|
962
|
+
...errors,
|
|
963
|
+
{
|
|
964
|
+
domain: context.domain,
|
|
965
|
+
path: context.path.join("."),
|
|
966
|
+
message: `Expected type 'string' for path, got type '${typeof value}'.`
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
let showRoot = true;
|
|
972
|
+
let ids = false;
|
|
973
|
+
let singleLevel = false;
|
|
974
|
+
let options;
|
|
975
|
+
if (isObject(field.options)) {
|
|
976
|
+
if (field.options.showRoot !== void 0) showRoot = field.options.showRoot;
|
|
977
|
+
if (field.options.ids !== void 0) ids = field.options.ids;
|
|
978
|
+
if (field.options.singleLevel !== void 0) singleLevel = field.options.singleLevel;
|
|
979
|
+
options = field.options.store;
|
|
980
|
+
} else {
|
|
981
|
+
options = field.options;
|
|
982
|
+
}
|
|
983
|
+
if (singleLevel && (!showRoot && value.includes("/") || showRoot && value.lastIndexOf("/") > 0)) {
|
|
984
|
+
return {
|
|
985
|
+
valid: false,
|
|
986
|
+
errors: [
|
|
987
|
+
...errors,
|
|
988
|
+
{
|
|
989
|
+
domain: context.domain,
|
|
990
|
+
path: context.path.join("."),
|
|
991
|
+
message: `Single level path cannot contain slashes.`
|
|
992
|
+
}
|
|
993
|
+
]
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
|
|
997
|
+
const levels = value.split("/");
|
|
998
|
+
if (levels[0] !== "") levels.unshift("");
|
|
999
|
+
if (showRoot && value === "/" && field.type === "folder") {
|
|
1000
|
+
return {
|
|
1001
|
+
valid: true,
|
|
1002
|
+
errors: []
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (levels.length < 2) {
|
|
1006
|
+
return {
|
|
1007
|
+
valid: false,
|
|
1008
|
+
errors: [
|
|
1009
|
+
...errors,
|
|
1010
|
+
{
|
|
1011
|
+
domain: context.domain,
|
|
1012
|
+
path: context.path.join("."),
|
|
1013
|
+
message: `Invalid path "${value}" encountered.`
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
const selectedPath = [];
|
|
1019
|
+
let levelOptions = [];
|
|
1020
|
+
for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
|
|
1021
|
+
const isLastLevel = levelIndex === levels.length - 2;
|
|
1022
|
+
const levelSelectedValue = levels[levelIndex + 1];
|
|
1023
|
+
const selectedPathValue = selectedPath.map(({ value: value2 }) => value2).join("/");
|
|
1024
|
+
if (!levelSelectedValue) {
|
|
1025
|
+
return {
|
|
1026
|
+
valid: false,
|
|
1027
|
+
errors: [
|
|
1028
|
+
...errors,
|
|
1029
|
+
{
|
|
1030
|
+
domain: context.domain,
|
|
1031
|
+
path: context.path.join("."),
|
|
1032
|
+
message: `Invalid selected value of "${value}" encountered.`
|
|
1033
|
+
}
|
|
1034
|
+
]
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
if (typeof options === "string") {
|
|
1038
|
+
try {
|
|
1039
|
+
levelOptions = await context.resolveRemote(options, context, {
|
|
1040
|
+
[field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
|
|
1041
|
+
});
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
return {
|
|
1044
|
+
valid: false,
|
|
1045
|
+
errors: [
|
|
1046
|
+
...errors,
|
|
1047
|
+
{
|
|
1048
|
+
domain: context.domain,
|
|
1049
|
+
path: context.path.join("."),
|
|
1050
|
+
message: `Failed to resolve remote resource ${options}: ${error}`
|
|
1051
|
+
}
|
|
1052
|
+
]
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
} else if (isLastLevel) {
|
|
1056
|
+
levelOptions = options;
|
|
1057
|
+
}
|
|
1058
|
+
const selectableOptions = levelOptions.flatMap((candidate) => {
|
|
1059
|
+
if (isLastLevel && (field.type === "file" && !candidate.file || field.type === "folder" && candidate.file)) {
|
|
1060
|
+
return [];
|
|
1061
|
+
}
|
|
1062
|
+
if (!isLastLevel && candidate.file) {
|
|
1063
|
+
return [];
|
|
1064
|
+
}
|
|
1065
|
+
return candidate;
|
|
1066
|
+
});
|
|
1067
|
+
const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
|
|
1068
|
+
if (!selectedOption) {
|
|
1069
|
+
return {
|
|
1070
|
+
valid: false,
|
|
1071
|
+
errors: [
|
|
1072
|
+
...errors,
|
|
1073
|
+
{
|
|
1074
|
+
domain: context.domain,
|
|
1075
|
+
path: context.path.join("."),
|
|
1076
|
+
message: `Path '${levelSelectedValue}' not found in options.`
|
|
1077
|
+
}
|
|
1078
|
+
]
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
selectedPath.push(selectedOption);
|
|
1082
|
+
}
|
|
1083
|
+
if (ids) {
|
|
1084
|
+
context.roots[context.domain].fieldStates.push({
|
|
1085
|
+
path: context.path,
|
|
1086
|
+
state: {
|
|
1087
|
+
mode: "chose",
|
|
1088
|
+
path: selectedPath.map(({ label, value: value2 }) => label ?? value2)
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
if (nested) {
|
|
1093
|
+
const result = await handleNestedFields(nested, value, field, context);
|
|
1094
|
+
errors.push(...result.errors);
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
valid: errors.length === 0,
|
|
1098
|
+
errors
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
async function handleSelectType(value, field, context) {
|
|
923
1102
|
const errors = [];
|
|
924
1103
|
let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
925
1104
|
let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
|
|
@@ -1191,6 +1370,19 @@ function toFormanSchema(field) {
|
|
|
1191
1370
|
}
|
|
1192
1371
|
return formanSchema;
|
|
1193
1372
|
case "string":
|
|
1373
|
+
const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
|
|
1374
|
+
if (pathInfo) {
|
|
1375
|
+
return {
|
|
1376
|
+
type: pathInfo.value.type,
|
|
1377
|
+
label: noEmpty(field.title),
|
|
1378
|
+
help: noEmpty(field.description),
|
|
1379
|
+
options: {
|
|
1380
|
+
store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
|
|
1381
|
+
showRoot: pathInfo.value.showRoot,
|
|
1382
|
+
singleLevel: pathInfo.value.singleLevel
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1194
1386
|
if (field.enum) {
|
|
1195
1387
|
return {
|
|
1196
1388
|
type: "select",
|
|
@@ -1206,7 +1398,7 @@ function toFormanSchema(field) {
|
|
|
1206
1398
|
options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
|
|
1207
1399
|
};
|
|
1208
1400
|
}
|
|
1209
|
-
|
|
1401
|
+
let textField = {
|
|
1210
1402
|
type: "text",
|
|
1211
1403
|
label: noEmpty(field.title),
|
|
1212
1404
|
help: noEmpty(field.description),
|
|
@@ -1217,9 +1409,12 @@ function toFormanSchema(field) {
|
|
|
1217
1409
|
if (field.pattern) textField.validate.pattern = field.pattern;
|
|
1218
1410
|
if (field.enum) textField.validate.enum = field.enum;
|
|
1219
1411
|
}
|
|
1412
|
+
if ("x-search" in field) {
|
|
1413
|
+
textField = handleSearchDirective(textField, field["x-search"]);
|
|
1414
|
+
}
|
|
1220
1415
|
return textField;
|
|
1221
1416
|
default:
|
|
1222
|
-
|
|
1417
|
+
let primitiveField = {
|
|
1223
1418
|
type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
|
|
1224
1419
|
label: noEmpty(field.title),
|
|
1225
1420
|
help: noEmpty(field.description),
|
|
@@ -1230,9 +1425,20 @@ function toFormanSchema(field) {
|
|
|
1230
1425
|
if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
|
|
1231
1426
|
if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
|
|
1232
1427
|
}
|
|
1428
|
+
if ("x-search" in field) {
|
|
1429
|
+
primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
|
|
1430
|
+
}
|
|
1233
1431
|
return primitiveField;
|
|
1234
1432
|
}
|
|
1235
1433
|
}
|
|
1434
|
+
function handleSearchDirective(formanField, directive) {
|
|
1435
|
+
formanField.rpc = {
|
|
1436
|
+
url: directive.url,
|
|
1437
|
+
label: directive.label,
|
|
1438
|
+
parameters: typeof directive.inputSchema.$ref === "string" ? directive.inputSchema.$ref : toFormanSchema(directive.inputSchema).spec
|
|
1439
|
+
};
|
|
1440
|
+
return formanField;
|
|
1441
|
+
}
|
|
1236
1442
|
|
|
1237
1443
|
// src/index.ts
|
|
1238
1444
|
function toJSONSchema(field) {
|
package/dist/index.d.cts
CHANGED
|
@@ -35,8 +35,8 @@ type FormanSchemaField = {
|
|
|
35
35
|
required?: boolean;
|
|
36
36
|
/** Default value for the field */
|
|
37
37
|
default?: FormanSchemaValue;
|
|
38
|
-
/** Available options for
|
|
39
|
-
options?: FormanSchemaOption[] | FormanSchemaOptionGroup[] | FormanSchemaExtendedOptions | string;
|
|
38
|
+
/** Available options for fields which support them */
|
|
39
|
+
options?: FormanSchemaOption[] | FormanSchemaOptionGroup[] | FormanSchemaDirectoryOption[] | FormanSchemaPathExtendedOptions | FormanSchemaExtendedOptions | string;
|
|
40
40
|
/** Help text or description for the field */
|
|
41
41
|
help?: string;
|
|
42
42
|
/** Sub-fields specification for collection or array types */
|
|
@@ -87,16 +87,42 @@ type FormanSchemaField = {
|
|
|
87
87
|
*/
|
|
88
88
|
type FormanSchemaRPCButton = {
|
|
89
89
|
/** RPC button label */
|
|
90
|
-
label
|
|
90
|
+
label?: string;
|
|
91
91
|
/** RPC button URL */
|
|
92
92
|
url: string;
|
|
93
93
|
/** RPC button parameters */
|
|
94
|
-
parameters: FormanSchemaField[];
|
|
94
|
+
parameters: FormanSchemaField[] | string;
|
|
95
95
|
};
|
|
96
96
|
/**
|
|
97
97
|
* Valid Forman Schema values
|
|
98
98
|
*/
|
|
99
99
|
type FormanSchemaValue = string | number | boolean | null;
|
|
100
|
+
/**
|
|
101
|
+
* Extended options for file and folder path selector fields
|
|
102
|
+
*/
|
|
103
|
+
type FormanSchemaPathExtendedOptions = {
|
|
104
|
+
/** Store for the options */
|
|
105
|
+
store: FormanSchemaDirectoryOption[] | string;
|
|
106
|
+
/** Nested fields for every option */
|
|
107
|
+
nested?: FormanSchemaNested;
|
|
108
|
+
/** When set to true, all paths start with '/' */
|
|
109
|
+
showRoot?: boolean;
|
|
110
|
+
/** Used when the directory entries have labels available, and these are different from their actual IDs */
|
|
111
|
+
ids?: boolean;
|
|
112
|
+
/** When set to true, only the entries from the root level are shown, without their children */
|
|
113
|
+
singleLevel?: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Directory entry option for file and folder path selectors
|
|
117
|
+
*/
|
|
118
|
+
type FormanSchemaDirectoryOption = {
|
|
119
|
+
/** Option value */
|
|
120
|
+
value: FormanSchemaValue;
|
|
121
|
+
/** Option label */
|
|
122
|
+
label?: string;
|
|
123
|
+
/** Set to true when the entry is a file in combined select mode */
|
|
124
|
+
file?: boolean;
|
|
125
|
+
};
|
|
100
126
|
/**
|
|
101
127
|
* Option for a select field
|
|
102
128
|
*/
|
|
@@ -171,6 +197,7 @@ type FormanValidationResult = {
|
|
|
171
197
|
type FormanSchemaFieldState = {
|
|
172
198
|
mode?: 'chose' | 'edit';
|
|
173
199
|
label?: string;
|
|
200
|
+
path?: Array<string>;
|
|
174
201
|
data?: Record<string, unknown>;
|
|
175
202
|
nested?: Record<string, FormanSchemaFieldState>;
|
|
176
203
|
items?: Record<string, FormanSchemaFieldState>[];
|
|
@@ -216,4 +243,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
216
243
|
*/
|
|
217
244
|
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
|
|
218
245
|
|
|
219
|
-
export { type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
|
|
246
|
+
export { type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
|
package/dist/index.d.ts
CHANGED
|
@@ -35,8 +35,8 @@ type FormanSchemaField = {
|
|
|
35
35
|
required?: boolean;
|
|
36
36
|
/** Default value for the field */
|
|
37
37
|
default?: FormanSchemaValue;
|
|
38
|
-
/** Available options for
|
|
39
|
-
options?: FormanSchemaOption[] | FormanSchemaOptionGroup[] | FormanSchemaExtendedOptions | string;
|
|
38
|
+
/** Available options for fields which support them */
|
|
39
|
+
options?: FormanSchemaOption[] | FormanSchemaOptionGroup[] | FormanSchemaDirectoryOption[] | FormanSchemaPathExtendedOptions | FormanSchemaExtendedOptions | string;
|
|
40
40
|
/** Help text or description for the field */
|
|
41
41
|
help?: string;
|
|
42
42
|
/** Sub-fields specification for collection or array types */
|
|
@@ -87,16 +87,42 @@ type FormanSchemaField = {
|
|
|
87
87
|
*/
|
|
88
88
|
type FormanSchemaRPCButton = {
|
|
89
89
|
/** RPC button label */
|
|
90
|
-
label
|
|
90
|
+
label?: string;
|
|
91
91
|
/** RPC button URL */
|
|
92
92
|
url: string;
|
|
93
93
|
/** RPC button parameters */
|
|
94
|
-
parameters: FormanSchemaField[];
|
|
94
|
+
parameters: FormanSchemaField[] | string;
|
|
95
95
|
};
|
|
96
96
|
/**
|
|
97
97
|
* Valid Forman Schema values
|
|
98
98
|
*/
|
|
99
99
|
type FormanSchemaValue = string | number | boolean | null;
|
|
100
|
+
/**
|
|
101
|
+
* Extended options for file and folder path selector fields
|
|
102
|
+
*/
|
|
103
|
+
type FormanSchemaPathExtendedOptions = {
|
|
104
|
+
/** Store for the options */
|
|
105
|
+
store: FormanSchemaDirectoryOption[] | string;
|
|
106
|
+
/** Nested fields for every option */
|
|
107
|
+
nested?: FormanSchemaNested;
|
|
108
|
+
/** When set to true, all paths start with '/' */
|
|
109
|
+
showRoot?: boolean;
|
|
110
|
+
/** Used when the directory entries have labels available, and these are different from their actual IDs */
|
|
111
|
+
ids?: boolean;
|
|
112
|
+
/** When set to true, only the entries from the root level are shown, without their children */
|
|
113
|
+
singleLevel?: boolean;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Directory entry option for file and folder path selectors
|
|
117
|
+
*/
|
|
118
|
+
type FormanSchemaDirectoryOption = {
|
|
119
|
+
/** Option value */
|
|
120
|
+
value: FormanSchemaValue;
|
|
121
|
+
/** Option label */
|
|
122
|
+
label?: string;
|
|
123
|
+
/** Set to true when the entry is a file in combined select mode */
|
|
124
|
+
file?: boolean;
|
|
125
|
+
};
|
|
100
126
|
/**
|
|
101
127
|
* Option for a select field
|
|
102
128
|
*/
|
|
@@ -171,6 +197,7 @@ type FormanValidationResult = {
|
|
|
171
197
|
type FormanSchemaFieldState = {
|
|
172
198
|
mode?: 'chose' | 'edit';
|
|
173
199
|
label?: string;
|
|
200
|
+
path?: Array<string>;
|
|
174
201
|
data?: Record<string, unknown>;
|
|
175
202
|
nested?: Record<string, FormanSchemaFieldState>;
|
|
176
203
|
items?: Record<string, FormanSchemaFieldState>[];
|
|
@@ -216,4 +243,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
216
243
|
*/
|
|
217
244
|
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
|
|
218
245
|
|
|
219
|
-
export { type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
|
|
246
|
+
export { type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
|
package/dist/index.js
CHANGED
|
@@ -287,11 +287,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
|
|
|
287
287
|
case "aiagent":
|
|
288
288
|
case "file":
|
|
289
289
|
case "folder":
|
|
290
|
-
return
|
|
290
|
+
return handleSelectOrPathType(normalizedField, result, context);
|
|
291
291
|
case "filter":
|
|
292
292
|
return handleFilterType(normalizedField, result, context);
|
|
293
293
|
default:
|
|
294
|
-
return handlePrimitiveType(normalizedField, result);
|
|
294
|
+
return handlePrimitiveType(normalizedField, result, context);
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
function handleCollectionType(field, result, context) {
|
|
@@ -407,8 +407,22 @@ function handleFilterType(field, result, context) {
|
|
|
407
407
|
});
|
|
408
408
|
return result;
|
|
409
409
|
}
|
|
410
|
-
function
|
|
410
|
+
function handleSelectOrPathType(field, result, context) {
|
|
411
411
|
const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
412
|
+
if (["file", "folder"].includes(field.type)) {
|
|
413
|
+
const optionsWrapper = isObject(field.options) ? field.options : void 0;
|
|
414
|
+
Object.defineProperty(result, "x-path", {
|
|
415
|
+
configurable: true,
|
|
416
|
+
enumerable: true,
|
|
417
|
+
writable: true,
|
|
418
|
+
value: {
|
|
419
|
+
type: field.type,
|
|
420
|
+
showRoot: optionsWrapper?.showRoot ?? true,
|
|
421
|
+
singleLevel: optionsWrapper?.singleLevel ?? false,
|
|
422
|
+
ownName: field.name
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
412
426
|
const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
|
|
413
427
|
const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
|
|
414
428
|
const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
|
|
@@ -517,9 +531,10 @@ function handleSelectType(field, result, context) {
|
|
|
517
531
|
)
|
|
518
532
|
});
|
|
519
533
|
}
|
|
534
|
+
if (field.rpc) result = processRpcDirective(field, result, context);
|
|
520
535
|
return result;
|
|
521
536
|
}
|
|
522
|
-
function handlePrimitiveType(field, result) {
|
|
537
|
+
function handlePrimitiveType(field, result, context) {
|
|
523
538
|
if (field.default !== "" && field.default != null) {
|
|
524
539
|
result.default = field.default;
|
|
525
540
|
}
|
|
@@ -537,6 +552,21 @@ function handlePrimitiveType(field, result) {
|
|
|
537
552
|
result.enum = field.validate.enum;
|
|
538
553
|
}
|
|
539
554
|
}
|
|
555
|
+
if (field.rpc) result = processRpcDirective(field, result, context);
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
function processRpcDirective(field, result, context) {
|
|
559
|
+
if (!field.rpc) return result;
|
|
560
|
+
Object.defineProperty(result, "x-search", {
|
|
561
|
+
configurable: true,
|
|
562
|
+
enumerable: true,
|
|
563
|
+
writable: true,
|
|
564
|
+
value: {
|
|
565
|
+
url: appendQueryString(field.rpc.url, context.domain, context.tail),
|
|
566
|
+
label: field.rpc.label,
|
|
567
|
+
inputSchema: typeof field.rpc.parameters === "string" ? { $ref: field.rpc.parameters } : toJSONSchemaInternal({ type: "collection", spec: field.rpc.parameters }, context)
|
|
568
|
+
}
|
|
569
|
+
});
|
|
540
570
|
return result;
|
|
541
571
|
}
|
|
542
572
|
|
|
@@ -625,7 +655,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
625
655
|
validateNestedFields: () => {
|
|
626
656
|
throw new Error("Cannot validate nested fields without parent field.");
|
|
627
657
|
},
|
|
628
|
-
resolveRemote: async (path, context) => {
|
|
658
|
+
resolveRemote: async (path, context, localData) => {
|
|
629
659
|
if (!options?.resolveRemote) {
|
|
630
660
|
throw new Error("Remote resource not supported when resolver is not provided.");
|
|
631
661
|
}
|
|
@@ -636,7 +666,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
636
666
|
},
|
|
637
667
|
{}
|
|
638
668
|
);
|
|
639
|
-
return await options.resolveRemote(path,
|
|
669
|
+
return await options.resolveRemote(path, {
|
|
670
|
+
...data,
|
|
671
|
+
...localData
|
|
672
|
+
});
|
|
640
673
|
}
|
|
641
674
|
}
|
|
642
675
|
);
|
|
@@ -723,9 +756,10 @@ async function validateFormanValue(value, field, context) {
|
|
|
723
756
|
case "aiagent":
|
|
724
757
|
case "udt":
|
|
725
758
|
case "scenario":
|
|
759
|
+
return handleSelectType(value, normalizedField, context);
|
|
726
760
|
case "file":
|
|
727
761
|
case "folder":
|
|
728
|
-
return
|
|
762
|
+
return handlePathType(value, normalizedField, context);
|
|
729
763
|
case "filter":
|
|
730
764
|
return handleFilterType2(value, normalizedField, context);
|
|
731
765
|
default:
|
|
@@ -890,7 +924,152 @@ async function handleFilterType2(value, field, context) {
|
|
|
890
924
|
};
|
|
891
925
|
return handleArrayType2(value, inlineSchema, context);
|
|
892
926
|
}
|
|
893
|
-
async function
|
|
927
|
+
async function handlePathType(value, field, context) {
|
|
928
|
+
const errors = [];
|
|
929
|
+
if (typeof value !== "string") {
|
|
930
|
+
return {
|
|
931
|
+
valid: false,
|
|
932
|
+
errors: [
|
|
933
|
+
...errors,
|
|
934
|
+
{
|
|
935
|
+
domain: context.domain,
|
|
936
|
+
path: context.path.join("."),
|
|
937
|
+
message: `Expected type 'string' for path, got type '${typeof value}'.`
|
|
938
|
+
}
|
|
939
|
+
]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
let showRoot = true;
|
|
943
|
+
let ids = false;
|
|
944
|
+
let singleLevel = false;
|
|
945
|
+
let options;
|
|
946
|
+
if (isObject(field.options)) {
|
|
947
|
+
if (field.options.showRoot !== void 0) showRoot = field.options.showRoot;
|
|
948
|
+
if (field.options.ids !== void 0) ids = field.options.ids;
|
|
949
|
+
if (field.options.singleLevel !== void 0) singleLevel = field.options.singleLevel;
|
|
950
|
+
options = field.options.store;
|
|
951
|
+
} else {
|
|
952
|
+
options = field.options;
|
|
953
|
+
}
|
|
954
|
+
if (singleLevel && (!showRoot && value.includes("/") || showRoot && value.lastIndexOf("/") > 0)) {
|
|
955
|
+
return {
|
|
956
|
+
valid: false,
|
|
957
|
+
errors: [
|
|
958
|
+
...errors,
|
|
959
|
+
{
|
|
960
|
+
domain: context.domain,
|
|
961
|
+
path: context.path.join("."),
|
|
962
|
+
message: `Single level path cannot contain slashes.`
|
|
963
|
+
}
|
|
964
|
+
]
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
|
|
968
|
+
const levels = value.split("/");
|
|
969
|
+
if (levels[0] !== "") levels.unshift("");
|
|
970
|
+
if (showRoot && value === "/" && field.type === "folder") {
|
|
971
|
+
return {
|
|
972
|
+
valid: true,
|
|
973
|
+
errors: []
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (levels.length < 2) {
|
|
977
|
+
return {
|
|
978
|
+
valid: false,
|
|
979
|
+
errors: [
|
|
980
|
+
...errors,
|
|
981
|
+
{
|
|
982
|
+
domain: context.domain,
|
|
983
|
+
path: context.path.join("."),
|
|
984
|
+
message: `Invalid path "${value}" encountered.`
|
|
985
|
+
}
|
|
986
|
+
]
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const selectedPath = [];
|
|
990
|
+
let levelOptions = [];
|
|
991
|
+
for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
|
|
992
|
+
const isLastLevel = levelIndex === levels.length - 2;
|
|
993
|
+
const levelSelectedValue = levels[levelIndex + 1];
|
|
994
|
+
const selectedPathValue = selectedPath.map(({ value: value2 }) => value2).join("/");
|
|
995
|
+
if (!levelSelectedValue) {
|
|
996
|
+
return {
|
|
997
|
+
valid: false,
|
|
998
|
+
errors: [
|
|
999
|
+
...errors,
|
|
1000
|
+
{
|
|
1001
|
+
domain: context.domain,
|
|
1002
|
+
path: context.path.join("."),
|
|
1003
|
+
message: `Invalid selected value of "${value}" encountered.`
|
|
1004
|
+
}
|
|
1005
|
+
]
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (typeof options === "string") {
|
|
1009
|
+
try {
|
|
1010
|
+
levelOptions = await context.resolveRemote(options, context, {
|
|
1011
|
+
[field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
|
|
1012
|
+
});
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
return {
|
|
1015
|
+
valid: false,
|
|
1016
|
+
errors: [
|
|
1017
|
+
...errors,
|
|
1018
|
+
{
|
|
1019
|
+
domain: context.domain,
|
|
1020
|
+
path: context.path.join("."),
|
|
1021
|
+
message: `Failed to resolve remote resource ${options}: ${error}`
|
|
1022
|
+
}
|
|
1023
|
+
]
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
} else if (isLastLevel) {
|
|
1027
|
+
levelOptions = options;
|
|
1028
|
+
}
|
|
1029
|
+
const selectableOptions = levelOptions.flatMap((candidate) => {
|
|
1030
|
+
if (isLastLevel && (field.type === "file" && !candidate.file || field.type === "folder" && candidate.file)) {
|
|
1031
|
+
return [];
|
|
1032
|
+
}
|
|
1033
|
+
if (!isLastLevel && candidate.file) {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
return candidate;
|
|
1037
|
+
});
|
|
1038
|
+
const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
|
|
1039
|
+
if (!selectedOption) {
|
|
1040
|
+
return {
|
|
1041
|
+
valid: false,
|
|
1042
|
+
errors: [
|
|
1043
|
+
...errors,
|
|
1044
|
+
{
|
|
1045
|
+
domain: context.domain,
|
|
1046
|
+
path: context.path.join("."),
|
|
1047
|
+
message: `Path '${levelSelectedValue}' not found in options.`
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
selectedPath.push(selectedOption);
|
|
1053
|
+
}
|
|
1054
|
+
if (ids) {
|
|
1055
|
+
context.roots[context.domain].fieldStates.push({
|
|
1056
|
+
path: context.path,
|
|
1057
|
+
state: {
|
|
1058
|
+
mode: "chose",
|
|
1059
|
+
path: selectedPath.map(({ label, value: value2 }) => label ?? value2)
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
if (nested) {
|
|
1064
|
+
const result = await handleNestedFields(nested, value, field, context);
|
|
1065
|
+
errors.push(...result.errors);
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
valid: errors.length === 0,
|
|
1069
|
+
errors
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
async function handleSelectType(value, field, context) {
|
|
894
1073
|
const errors = [];
|
|
895
1074
|
let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
|
|
896
1075
|
let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
|
|
@@ -1162,6 +1341,19 @@ function toFormanSchema(field) {
|
|
|
1162
1341
|
}
|
|
1163
1342
|
return formanSchema;
|
|
1164
1343
|
case "string":
|
|
1344
|
+
const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
|
|
1345
|
+
if (pathInfo) {
|
|
1346
|
+
return {
|
|
1347
|
+
type: pathInfo.value.type,
|
|
1348
|
+
label: noEmpty(field.title),
|
|
1349
|
+
help: noEmpty(field.description),
|
|
1350
|
+
options: {
|
|
1351
|
+
store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
|
|
1352
|
+
showRoot: pathInfo.value.showRoot,
|
|
1353
|
+
singleLevel: pathInfo.value.singleLevel
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1165
1357
|
if (field.enum) {
|
|
1166
1358
|
return {
|
|
1167
1359
|
type: "select",
|
|
@@ -1177,7 +1369,7 @@ function toFormanSchema(field) {
|
|
|
1177
1369
|
options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
|
|
1178
1370
|
};
|
|
1179
1371
|
}
|
|
1180
|
-
|
|
1372
|
+
let textField = {
|
|
1181
1373
|
type: "text",
|
|
1182
1374
|
label: noEmpty(field.title),
|
|
1183
1375
|
help: noEmpty(field.description),
|
|
@@ -1188,9 +1380,12 @@ function toFormanSchema(field) {
|
|
|
1188
1380
|
if (field.pattern) textField.validate.pattern = field.pattern;
|
|
1189
1381
|
if (field.enum) textField.validate.enum = field.enum;
|
|
1190
1382
|
}
|
|
1383
|
+
if ("x-search" in field) {
|
|
1384
|
+
textField = handleSearchDirective(textField, field["x-search"]);
|
|
1385
|
+
}
|
|
1191
1386
|
return textField;
|
|
1192
1387
|
default:
|
|
1193
|
-
|
|
1388
|
+
let primitiveField = {
|
|
1194
1389
|
type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
|
|
1195
1390
|
label: noEmpty(field.title),
|
|
1196
1391
|
help: noEmpty(field.description),
|
|
@@ -1201,9 +1396,20 @@ function toFormanSchema(field) {
|
|
|
1201
1396
|
if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
|
|
1202
1397
|
if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
|
|
1203
1398
|
}
|
|
1399
|
+
if ("x-search" in field) {
|
|
1400
|
+
primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
|
|
1401
|
+
}
|
|
1204
1402
|
return primitiveField;
|
|
1205
1403
|
}
|
|
1206
1404
|
}
|
|
1405
|
+
function handleSearchDirective(formanField, directive) {
|
|
1406
|
+
formanField.rpc = {
|
|
1407
|
+
url: directive.url,
|
|
1408
|
+
label: directive.label,
|
|
1409
|
+
parameters: typeof directive.inputSchema.$ref === "string" ? directive.inputSchema.$ref : toFormanSchema(directive.inputSchema).spec
|
|
1410
|
+
};
|
|
1411
|
+
return formanField;
|
|
1412
|
+
}
|
|
1207
1413
|
|
|
1208
1414
|
// src/index.ts
|
|
1209
1415
|
function toJSONSchema(field) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makehq/forman-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Forman Schema Tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Make",
|
|
7
|
-
"repository":
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/integromat/make-forman-schema.git"
|
|
10
|
+
},
|
|
8
11
|
"homepage": "https://www.make.com",
|
|
9
12
|
"type": "module",
|
|
10
13
|
"main": "./dist/index.js",
|