@makehq/forman-schema 1.7.1 → 1.8.1

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 CHANGED
@@ -89,6 +89,15 @@ function normalizeFormanFieldType(field) {
89
89
  }
90
90
  };
91
91
  }
92
+ function findValueInSelectOptions(field, value, optionsAndGroups) {
93
+ if (!optionsAndGroups) return void 0;
94
+ if (field.grouped) {
95
+ const found2 = optionsAndGroups.flatMap((group) => "options" in group ? group.options : []).find((option) => option.value === value);
96
+ if (found2) return found2;
97
+ }
98
+ const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
99
+ return found;
100
+ }
92
101
  function buildRestoreStructure(items) {
93
102
  const result = {};
94
103
  for (const item of items) {
@@ -316,11 +325,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
316
325
  case "aiagent":
317
326
  case "file":
318
327
  case "folder":
319
- return handleSelectType(normalizedField, result, context);
328
+ return handleSelectOrPathType(normalizedField, result, context);
320
329
  case "filter":
321
330
  return handleFilterType(normalizedField, result, context);
322
331
  default:
323
- return handlePrimitiveType(normalizedField, result);
332
+ return handlePrimitiveType(normalizedField, result, context);
324
333
  }
325
334
  }
326
335
  function handleCollectionType(field, result, context) {
@@ -436,8 +445,22 @@ function handleFilterType(field, result, context) {
436
445
  });
437
446
  return result;
438
447
  }
439
- function handleSelectType(field, result, context) {
448
+ function handleSelectOrPathType(field, result, context) {
440
449
  const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
450
+ if (["file", "folder"].includes(field.type)) {
451
+ const optionsWrapper = isObject(field.options) ? field.options : void 0;
452
+ Object.defineProperty(result, "x-path", {
453
+ configurable: true,
454
+ enumerable: true,
455
+ writable: true,
456
+ value: {
457
+ type: field.type,
458
+ showRoot: optionsWrapper?.showRoot ?? true,
459
+ singleLevel: optionsWrapper?.singleLevel ?? false,
460
+ ownName: field.name
461
+ }
462
+ });
463
+ }
441
464
  const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
442
465
  const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
443
466
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
@@ -449,12 +472,15 @@ function handleSelectType(field, result, context) {
449
472
  value: appendQueryString(optionsOrGroups, context.domain, context.tail)
450
473
  });
451
474
  } else {
452
- const options = optionsOrGroups?.some(isOptionGroup) ? optionsOrGroups.flatMap(
453
- (group) => group.options.map((option) => ({
454
- ...option,
455
- label: `${group.label}: ${option.label || option.value}`
456
- }))
457
- ) : optionsOrGroups || [];
475
+ const options = optionsOrGroups?.flatMap((optionOrGroup) => {
476
+ if (isOptionGroup(optionOrGroup)) {
477
+ return optionOrGroup.options.map((option) => ({
478
+ ...option,
479
+ label: `${optionOrGroup.label}: ${option.label || option.value}`
480
+ }));
481
+ }
482
+ return optionOrGroup;
483
+ });
458
484
  if (options?.some((option) => {
459
485
  if (isOptionGroup(option)) return option.options.some((option2) => option2.label || option2.nested);
460
486
  return option.label || option.nested;
@@ -546,9 +572,10 @@ function handleSelectType(field, result, context) {
546
572
  )
547
573
  });
548
574
  }
575
+ if (field.rpc) result = processRpcDirective(field, result, context);
549
576
  return result;
550
577
  }
551
- function handlePrimitiveType(field, result) {
578
+ function handlePrimitiveType(field, result, context) {
552
579
  if (field.default !== "" && field.default != null) {
553
580
  result.default = field.default;
554
581
  }
@@ -566,6 +593,21 @@ function handlePrimitiveType(field, result) {
566
593
  result.enum = field.validate.enum;
567
594
  }
568
595
  }
596
+ if (field.rpc) result = processRpcDirective(field, result, context);
597
+ return result;
598
+ }
599
+ function processRpcDirective(field, result, context) {
600
+ if (!field.rpc) return result;
601
+ Object.defineProperty(result, "x-search", {
602
+ configurable: true,
603
+ enumerable: true,
604
+ writable: true,
605
+ value: {
606
+ url: appendQueryString(field.rpc.url, context.domain, context.tail),
607
+ label: field.rpc.label,
608
+ inputSchema: typeof field.rpc.parameters === "string" ? { $ref: field.rpc.parameters } : toJSONSchemaInternal({ type: "collection", spec: field.rpc.parameters }, context)
609
+ }
610
+ });
569
611
  return result;
570
612
  }
571
613
 
@@ -654,7 +696,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
654
696
  validateNestedFields: () => {
655
697
  throw new Error("Cannot validate nested fields without parent field.");
656
698
  },
657
- resolveRemote: async (path, context) => {
699
+ resolveRemote: async (path, context, localData) => {
658
700
  if (!options?.resolveRemote) {
659
701
  throw new Error("Remote resource not supported when resolver is not provided.");
660
702
  }
@@ -665,7 +707,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
665
707
  },
666
708
  {}
667
709
  );
668
- return await options.resolveRemote(path, data);
710
+ return await options.resolveRemote(path, {
711
+ ...data,
712
+ ...localData
713
+ });
669
714
  }
670
715
  }
671
716
  );
@@ -752,9 +797,10 @@ async function validateFormanValue(value, field, context) {
752
797
  case "aiagent":
753
798
  case "udt":
754
799
  case "scenario":
800
+ return handleSelectType(value, normalizedField, context);
755
801
  case "file":
756
802
  case "folder":
757
- return handleSelectType2(value, normalizedField, context);
803
+ return handlePathType(value, normalizedField, context);
758
804
  case "filter":
759
805
  return handleFilterType2(value, normalizedField, context);
760
806
  default:
@@ -919,7 +965,152 @@ async function handleFilterType2(value, field, context) {
919
965
  };
920
966
  return handleArrayType2(value, inlineSchema, context);
921
967
  }
922
- async function handleSelectType2(value, field, context) {
968
+ async function handlePathType(value, field, context) {
969
+ const errors = [];
970
+ if (typeof value !== "string") {
971
+ return {
972
+ valid: false,
973
+ errors: [
974
+ ...errors,
975
+ {
976
+ domain: context.domain,
977
+ path: context.path.join("."),
978
+ message: `Expected type 'string' for path, got type '${typeof value}'.`
979
+ }
980
+ ]
981
+ };
982
+ }
983
+ let showRoot = true;
984
+ let ids = false;
985
+ let singleLevel = false;
986
+ let options;
987
+ if (isObject(field.options)) {
988
+ if (field.options.showRoot !== void 0) showRoot = field.options.showRoot;
989
+ if (field.options.ids !== void 0) ids = field.options.ids;
990
+ if (field.options.singleLevel !== void 0) singleLevel = field.options.singleLevel;
991
+ options = field.options.store;
992
+ } else {
993
+ options = field.options;
994
+ }
995
+ if (singleLevel && (!showRoot && value.includes("/") || showRoot && value.lastIndexOf("/") > 0)) {
996
+ return {
997
+ valid: false,
998
+ errors: [
999
+ ...errors,
1000
+ {
1001
+ domain: context.domain,
1002
+ path: context.path.join("."),
1003
+ message: `Single level path cannot contain slashes.`
1004
+ }
1005
+ ]
1006
+ };
1007
+ }
1008
+ let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
1009
+ const levels = value.split("/");
1010
+ if (levels[0] !== "") levels.unshift("");
1011
+ if (showRoot && value === "/" && field.type === "folder") {
1012
+ return {
1013
+ valid: true,
1014
+ errors: []
1015
+ };
1016
+ }
1017
+ if (levels.length < 2) {
1018
+ return {
1019
+ valid: false,
1020
+ errors: [
1021
+ ...errors,
1022
+ {
1023
+ domain: context.domain,
1024
+ path: context.path.join("."),
1025
+ message: `Invalid path "${value}" encountered.`
1026
+ }
1027
+ ]
1028
+ };
1029
+ }
1030
+ const selectedPath = [];
1031
+ let levelOptions = [];
1032
+ for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
1033
+ const isLastLevel = levelIndex === levels.length - 2;
1034
+ const levelSelectedValue = levels[levelIndex + 1];
1035
+ const selectedPathValue = selectedPath.map(({ value: value2 }) => value2).join("/");
1036
+ if (!levelSelectedValue) {
1037
+ return {
1038
+ valid: false,
1039
+ errors: [
1040
+ ...errors,
1041
+ {
1042
+ domain: context.domain,
1043
+ path: context.path.join("."),
1044
+ message: `Invalid selected value of "${value}" encountered.`
1045
+ }
1046
+ ]
1047
+ };
1048
+ }
1049
+ if (typeof options === "string") {
1050
+ try {
1051
+ levelOptions = await context.resolveRemote(options, context, {
1052
+ [field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
1053
+ });
1054
+ } catch (error) {
1055
+ return {
1056
+ valid: false,
1057
+ errors: [
1058
+ ...errors,
1059
+ {
1060
+ domain: context.domain,
1061
+ path: context.path.join("."),
1062
+ message: `Failed to resolve remote resource ${options}: ${error}`
1063
+ }
1064
+ ]
1065
+ };
1066
+ }
1067
+ } else if (isLastLevel) {
1068
+ levelOptions = options;
1069
+ }
1070
+ const selectableOptions = levelOptions.flatMap((candidate) => {
1071
+ if (isLastLevel && (field.type === "file" && !candidate.file || field.type === "folder" && candidate.file)) {
1072
+ return [];
1073
+ }
1074
+ if (!isLastLevel && candidate.file) {
1075
+ return [];
1076
+ }
1077
+ return candidate;
1078
+ });
1079
+ const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
1080
+ if (!selectedOption) {
1081
+ return {
1082
+ valid: false,
1083
+ errors: [
1084
+ ...errors,
1085
+ {
1086
+ domain: context.domain,
1087
+ path: context.path.join("."),
1088
+ message: `Path '${levelSelectedValue}' not found in options.`
1089
+ }
1090
+ ]
1091
+ };
1092
+ }
1093
+ selectedPath.push(selectedOption);
1094
+ }
1095
+ if (ids) {
1096
+ context.roots[context.domain].fieldStates.push({
1097
+ path: context.path,
1098
+ state: {
1099
+ mode: "chose",
1100
+ path: selectedPath.map(({ label, value: value2 }) => label ?? value2)
1101
+ }
1102
+ });
1103
+ }
1104
+ if (nested) {
1105
+ const result = await handleNestedFields(nested, value, field, context);
1106
+ errors.push(...result.errors);
1107
+ }
1108
+ return {
1109
+ valid: errors.length === 0,
1110
+ errors
1111
+ };
1112
+ }
1113
+ async function handleSelectType(value, field, context) {
923
1114
  const errors = [];
924
1115
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
925
1116
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -955,9 +1146,11 @@ async function handleSelectType2(value, field, context) {
955
1146
  };
956
1147
  }
957
1148
  for (const singleValue of value) {
958
- const found = field.grouped ? optionsOrGroups.some(
959
- (group) => group.options.some((option) => option.value === singleValue)
960
- ) : optionsOrGroups.some((option) => option.value === singleValue);
1149
+ const found = findValueInSelectOptions(
1150
+ field,
1151
+ singleValue,
1152
+ optionsOrGroups
1153
+ );
961
1154
  if (!found) {
962
1155
  errors.push({
963
1156
  domain: context.domain,
@@ -983,7 +1176,7 @@ async function handleSelectType2(value, field, context) {
983
1176
  }
984
1177
  }
985
1178
  } else {
986
- const item = field.grouped ? optionsOrGroups.find((group) => group.options.some((option) => option.value === value))?.options.find((option) => option.value === value) : optionsOrGroups.find((option) => option.value === value);
1179
+ const item = findValueInSelectOptions(field, value, optionsOrGroups);
987
1180
  if (!item) {
988
1181
  return {
989
1182
  valid: false,
@@ -1191,6 +1384,19 @@ function toFormanSchema(field) {
1191
1384
  }
1192
1385
  return formanSchema;
1193
1386
  case "string":
1387
+ const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
1388
+ if (pathInfo) {
1389
+ return {
1390
+ type: pathInfo.value.type,
1391
+ label: noEmpty(field.title),
1392
+ help: noEmpty(field.description),
1393
+ options: {
1394
+ store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
1395
+ showRoot: pathInfo.value.showRoot,
1396
+ singleLevel: pathInfo.value.singleLevel
1397
+ }
1398
+ };
1399
+ }
1194
1400
  if (field.enum) {
1195
1401
  return {
1196
1402
  type: "select",
@@ -1206,7 +1412,7 @@ function toFormanSchema(field) {
1206
1412
  options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1207
1413
  };
1208
1414
  }
1209
- const textField = {
1415
+ let textField = {
1210
1416
  type: "text",
1211
1417
  label: noEmpty(field.title),
1212
1418
  help: noEmpty(field.description),
@@ -1217,9 +1423,12 @@ function toFormanSchema(field) {
1217
1423
  if (field.pattern) textField.validate.pattern = field.pattern;
1218
1424
  if (field.enum) textField.validate.enum = field.enum;
1219
1425
  }
1426
+ if ("x-search" in field) {
1427
+ textField = handleSearchDirective(textField, field["x-search"]);
1428
+ }
1220
1429
  return textField;
1221
1430
  default:
1222
- const primitiveField = {
1431
+ let primitiveField = {
1223
1432
  type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
1224
1433
  label: noEmpty(field.title),
1225
1434
  help: noEmpty(field.description),
@@ -1230,9 +1439,20 @@ function toFormanSchema(field) {
1230
1439
  if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
1231
1440
  if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
1232
1441
  }
1442
+ if ("x-search" in field) {
1443
+ primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
1444
+ }
1233
1445
  return primitiveField;
1234
1446
  }
1235
1447
  }
1448
+ function handleSearchDirective(formanField, directive) {
1449
+ formanField.rpc = {
1450
+ url: directive.url,
1451
+ label: directive.label,
1452
+ parameters: typeof directive.inputSchema.$ref === "string" ? directive.inputSchema.$ref : toFormanSchema(directive.inputSchema).spec
1453
+ };
1454
+ return formanField;
1455
+ }
1236
1456
 
1237
1457
  // src/index.ts
1238
1458
  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 select type fields */
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: string;
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 select type fields */
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: string;
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
@@ -60,6 +60,15 @@ function normalizeFormanFieldType(field) {
60
60
  }
61
61
  };
62
62
  }
63
+ function findValueInSelectOptions(field, value, optionsAndGroups) {
64
+ if (!optionsAndGroups) return void 0;
65
+ if (field.grouped) {
66
+ const found2 = optionsAndGroups.flatMap((group) => "options" in group ? group.options : []).find((option) => option.value === value);
67
+ if (found2) return found2;
68
+ }
69
+ const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
70
+ return found;
71
+ }
63
72
  function buildRestoreStructure(items) {
64
73
  const result = {};
65
74
  for (const item of items) {
@@ -287,11 +296,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
287
296
  case "aiagent":
288
297
  case "file":
289
298
  case "folder":
290
- return handleSelectType(normalizedField, result, context);
299
+ return handleSelectOrPathType(normalizedField, result, context);
291
300
  case "filter":
292
301
  return handleFilterType(normalizedField, result, context);
293
302
  default:
294
- return handlePrimitiveType(normalizedField, result);
303
+ return handlePrimitiveType(normalizedField, result, context);
295
304
  }
296
305
  }
297
306
  function handleCollectionType(field, result, context) {
@@ -407,8 +416,22 @@ function handleFilterType(field, result, context) {
407
416
  });
408
417
  return result;
409
418
  }
410
- function handleSelectType(field, result, context) {
419
+ function handleSelectOrPathType(field, result, context) {
411
420
  const optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
421
+ if (["file", "folder"].includes(field.type)) {
422
+ const optionsWrapper = isObject(field.options) ? field.options : void 0;
423
+ Object.defineProperty(result, "x-path", {
424
+ configurable: true,
425
+ enumerable: true,
426
+ writable: true,
427
+ value: {
428
+ type: field.type,
429
+ showRoot: optionsWrapper?.showRoot ?? true,
430
+ singleLevel: optionsWrapper?.singleLevel ?? false,
431
+ ownName: field.name
432
+ }
433
+ });
434
+ }
412
435
  const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
413
436
  const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
414
437
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
@@ -420,12 +443,15 @@ function handleSelectType(field, result, context) {
420
443
  value: appendQueryString(optionsOrGroups, context.domain, context.tail)
421
444
  });
422
445
  } else {
423
- const options = optionsOrGroups?.some(isOptionGroup) ? optionsOrGroups.flatMap(
424
- (group) => group.options.map((option) => ({
425
- ...option,
426
- label: `${group.label}: ${option.label || option.value}`
427
- }))
428
- ) : optionsOrGroups || [];
446
+ const options = optionsOrGroups?.flatMap((optionOrGroup) => {
447
+ if (isOptionGroup(optionOrGroup)) {
448
+ return optionOrGroup.options.map((option) => ({
449
+ ...option,
450
+ label: `${optionOrGroup.label}: ${option.label || option.value}`
451
+ }));
452
+ }
453
+ return optionOrGroup;
454
+ });
429
455
  if (options?.some((option) => {
430
456
  if (isOptionGroup(option)) return option.options.some((option2) => option2.label || option2.nested);
431
457
  return option.label || option.nested;
@@ -517,9 +543,10 @@ function handleSelectType(field, result, context) {
517
543
  )
518
544
  });
519
545
  }
546
+ if (field.rpc) result = processRpcDirective(field, result, context);
520
547
  return result;
521
548
  }
522
- function handlePrimitiveType(field, result) {
549
+ function handlePrimitiveType(field, result, context) {
523
550
  if (field.default !== "" && field.default != null) {
524
551
  result.default = field.default;
525
552
  }
@@ -537,6 +564,21 @@ function handlePrimitiveType(field, result) {
537
564
  result.enum = field.validate.enum;
538
565
  }
539
566
  }
567
+ if (field.rpc) result = processRpcDirective(field, result, context);
568
+ return result;
569
+ }
570
+ function processRpcDirective(field, result, context) {
571
+ if (!field.rpc) return result;
572
+ Object.defineProperty(result, "x-search", {
573
+ configurable: true,
574
+ enumerable: true,
575
+ writable: true,
576
+ value: {
577
+ url: appendQueryString(field.rpc.url, context.domain, context.tail),
578
+ label: field.rpc.label,
579
+ inputSchema: typeof field.rpc.parameters === "string" ? { $ref: field.rpc.parameters } : toJSONSchemaInternal({ type: "collection", spec: field.rpc.parameters }, context)
580
+ }
581
+ });
540
582
  return result;
541
583
  }
542
584
 
@@ -625,7 +667,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
625
667
  validateNestedFields: () => {
626
668
  throw new Error("Cannot validate nested fields without parent field.");
627
669
  },
628
- resolveRemote: async (path, context) => {
670
+ resolveRemote: async (path, context, localData) => {
629
671
  if (!options?.resolveRemote) {
630
672
  throw new Error("Remote resource not supported when resolver is not provided.");
631
673
  }
@@ -636,7 +678,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
636
678
  },
637
679
  {}
638
680
  );
639
- return await options.resolveRemote(path, data);
681
+ return await options.resolveRemote(path, {
682
+ ...data,
683
+ ...localData
684
+ });
640
685
  }
641
686
  }
642
687
  );
@@ -723,9 +768,10 @@ async function validateFormanValue(value, field, context) {
723
768
  case "aiagent":
724
769
  case "udt":
725
770
  case "scenario":
771
+ return handleSelectType(value, normalizedField, context);
726
772
  case "file":
727
773
  case "folder":
728
- return handleSelectType2(value, normalizedField, context);
774
+ return handlePathType(value, normalizedField, context);
729
775
  case "filter":
730
776
  return handleFilterType2(value, normalizedField, context);
731
777
  default:
@@ -890,7 +936,152 @@ async function handleFilterType2(value, field, context) {
890
936
  };
891
937
  return handleArrayType2(value, inlineSchema, context);
892
938
  }
893
- async function handleSelectType2(value, field, context) {
939
+ async function handlePathType(value, field, context) {
940
+ const errors = [];
941
+ if (typeof value !== "string") {
942
+ return {
943
+ valid: false,
944
+ errors: [
945
+ ...errors,
946
+ {
947
+ domain: context.domain,
948
+ path: context.path.join("."),
949
+ message: `Expected type 'string' for path, got type '${typeof value}'.`
950
+ }
951
+ ]
952
+ };
953
+ }
954
+ let showRoot = true;
955
+ let ids = false;
956
+ let singleLevel = false;
957
+ let options;
958
+ if (isObject(field.options)) {
959
+ if (field.options.showRoot !== void 0) showRoot = field.options.showRoot;
960
+ if (field.options.ids !== void 0) ids = field.options.ids;
961
+ if (field.options.singleLevel !== void 0) singleLevel = field.options.singleLevel;
962
+ options = field.options.store;
963
+ } else {
964
+ options = field.options;
965
+ }
966
+ if (singleLevel && (!showRoot && value.includes("/") || showRoot && value.lastIndexOf("/") > 0)) {
967
+ return {
968
+ valid: false,
969
+ errors: [
970
+ ...errors,
971
+ {
972
+ domain: context.domain,
973
+ path: context.path.join("."),
974
+ message: `Single level path cannot contain slashes.`
975
+ }
976
+ ]
977
+ };
978
+ }
979
+ let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
980
+ const levels = value.split("/");
981
+ if (levels[0] !== "") levels.unshift("");
982
+ if (showRoot && value === "/" && field.type === "folder") {
983
+ return {
984
+ valid: true,
985
+ errors: []
986
+ };
987
+ }
988
+ if (levels.length < 2) {
989
+ return {
990
+ valid: false,
991
+ errors: [
992
+ ...errors,
993
+ {
994
+ domain: context.domain,
995
+ path: context.path.join("."),
996
+ message: `Invalid path "${value}" encountered.`
997
+ }
998
+ ]
999
+ };
1000
+ }
1001
+ const selectedPath = [];
1002
+ let levelOptions = [];
1003
+ for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
1004
+ const isLastLevel = levelIndex === levels.length - 2;
1005
+ const levelSelectedValue = levels[levelIndex + 1];
1006
+ const selectedPathValue = selectedPath.map(({ value: value2 }) => value2).join("/");
1007
+ if (!levelSelectedValue) {
1008
+ return {
1009
+ valid: false,
1010
+ errors: [
1011
+ ...errors,
1012
+ {
1013
+ domain: context.domain,
1014
+ path: context.path.join("."),
1015
+ message: `Invalid selected value of "${value}" encountered.`
1016
+ }
1017
+ ]
1018
+ };
1019
+ }
1020
+ if (typeof options === "string") {
1021
+ try {
1022
+ levelOptions = await context.resolveRemote(options, context, {
1023
+ [field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
1024
+ });
1025
+ } catch (error) {
1026
+ return {
1027
+ valid: false,
1028
+ errors: [
1029
+ ...errors,
1030
+ {
1031
+ domain: context.domain,
1032
+ path: context.path.join("."),
1033
+ message: `Failed to resolve remote resource ${options}: ${error}`
1034
+ }
1035
+ ]
1036
+ };
1037
+ }
1038
+ } else if (isLastLevel) {
1039
+ levelOptions = options;
1040
+ }
1041
+ const selectableOptions = levelOptions.flatMap((candidate) => {
1042
+ if (isLastLevel && (field.type === "file" && !candidate.file || field.type === "folder" && candidate.file)) {
1043
+ return [];
1044
+ }
1045
+ if (!isLastLevel && candidate.file) {
1046
+ return [];
1047
+ }
1048
+ return candidate;
1049
+ });
1050
+ const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
1051
+ if (!selectedOption) {
1052
+ return {
1053
+ valid: false,
1054
+ errors: [
1055
+ ...errors,
1056
+ {
1057
+ domain: context.domain,
1058
+ path: context.path.join("."),
1059
+ message: `Path '${levelSelectedValue}' not found in options.`
1060
+ }
1061
+ ]
1062
+ };
1063
+ }
1064
+ selectedPath.push(selectedOption);
1065
+ }
1066
+ if (ids) {
1067
+ context.roots[context.domain].fieldStates.push({
1068
+ path: context.path,
1069
+ state: {
1070
+ mode: "chose",
1071
+ path: selectedPath.map(({ label, value: value2 }) => label ?? value2)
1072
+ }
1073
+ });
1074
+ }
1075
+ if (nested) {
1076
+ const result = await handleNestedFields(nested, value, field, context);
1077
+ errors.push(...result.errors);
1078
+ }
1079
+ return {
1080
+ valid: errors.length === 0,
1081
+ errors
1082
+ };
1083
+ }
1084
+ async function handleSelectType(value, field, context) {
894
1085
  const errors = [];
895
1086
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
896
1087
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -926,9 +1117,11 @@ async function handleSelectType2(value, field, context) {
926
1117
  };
927
1118
  }
928
1119
  for (const singleValue of value) {
929
- const found = field.grouped ? optionsOrGroups.some(
930
- (group) => group.options.some((option) => option.value === singleValue)
931
- ) : optionsOrGroups.some((option) => option.value === singleValue);
1120
+ const found = findValueInSelectOptions(
1121
+ field,
1122
+ singleValue,
1123
+ optionsOrGroups
1124
+ );
932
1125
  if (!found) {
933
1126
  errors.push({
934
1127
  domain: context.domain,
@@ -954,7 +1147,7 @@ async function handleSelectType2(value, field, context) {
954
1147
  }
955
1148
  }
956
1149
  } else {
957
- const item = field.grouped ? optionsOrGroups.find((group) => group.options.some((option) => option.value === value))?.options.find((option) => option.value === value) : optionsOrGroups.find((option) => option.value === value);
1150
+ const item = findValueInSelectOptions(field, value, optionsOrGroups);
958
1151
  if (!item) {
959
1152
  return {
960
1153
  valid: false,
@@ -1162,6 +1355,19 @@ function toFormanSchema(field) {
1162
1355
  }
1163
1356
  return formanSchema;
1164
1357
  case "string":
1358
+ const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
1359
+ if (pathInfo) {
1360
+ return {
1361
+ type: pathInfo.value.type,
1362
+ label: noEmpty(field.title),
1363
+ help: noEmpty(field.description),
1364
+ options: {
1365
+ store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
1366
+ showRoot: pathInfo.value.showRoot,
1367
+ singleLevel: pathInfo.value.singleLevel
1368
+ }
1369
+ };
1370
+ }
1165
1371
  if (field.enum) {
1166
1372
  return {
1167
1373
  type: "select",
@@ -1177,7 +1383,7 @@ function toFormanSchema(field) {
1177
1383
  options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1178
1384
  };
1179
1385
  }
1180
- const textField = {
1386
+ let textField = {
1181
1387
  type: "text",
1182
1388
  label: noEmpty(field.title),
1183
1389
  help: noEmpty(field.description),
@@ -1188,9 +1394,12 @@ function toFormanSchema(field) {
1188
1394
  if (field.pattern) textField.validate.pattern = field.pattern;
1189
1395
  if (field.enum) textField.validate.enum = field.enum;
1190
1396
  }
1397
+ if ("x-search" in field) {
1398
+ textField = handleSearchDirective(textField, field["x-search"]);
1399
+ }
1191
1400
  return textField;
1192
1401
  default:
1193
- const primitiveField = {
1402
+ let primitiveField = {
1194
1403
  type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
1195
1404
  label: noEmpty(field.title),
1196
1405
  help: noEmpty(field.description),
@@ -1201,9 +1410,20 @@ function toFormanSchema(field) {
1201
1410
  if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
1202
1411
  if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
1203
1412
  }
1413
+ if ("x-search" in field) {
1414
+ primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
1415
+ }
1204
1416
  return primitiveField;
1205
1417
  }
1206
1418
  }
1419
+ function handleSearchDirective(formanField, directive) {
1420
+ formanField.rpc = {
1421
+ url: directive.url,
1422
+ label: directive.label,
1423
+ parameters: typeof directive.inputSchema.$ref === "string" ? directive.inputSchema.$ref : toFormanSchema(directive.inputSchema).spec
1424
+ };
1425
+ return formanField;
1426
+ }
1207
1427
 
1208
1428
  // src/index.ts
1209
1429
  function toJSONSchema(field) {
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",
7
- "repository": "github:integromat/forman-schema",
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",