@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 CHANGED
@@ -316,11 +316,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
316
316
  case "aiagent":
317
317
  case "file":
318
318
  case "folder":
319
- return handleSelectType(normalizedField, result, context);
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 handleSelectType(field, result, context) {
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, data);
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 handleSelectType2(value, normalizedField, context);
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 handleSelectType2(value, field, context) {
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
- const textField = {
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
- const primitiveField = {
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 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
@@ -287,11 +287,11 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
287
287
  case "aiagent":
288
288
  case "file":
289
289
  case "folder":
290
- return handleSelectType(normalizedField, result, context);
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 handleSelectType(field, result, context) {
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, data);
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 handleSelectType2(value, normalizedField, context);
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 handleSelectType2(value, field, context) {
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
- const textField = {
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
- const primitiveField = {
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.7.1",
3
+ "version": "1.8.0",
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",