@makehq/forman-schema 1.7.0 → 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
@@ -67,7 +67,7 @@ var API_ENDPOINTS = {
67
67
  aiagent: "api://ai-agents/v1/agents",
68
68
  datastore: "api://data-stores",
69
69
  hook: "api://hooks/{{kind}}",
70
- device: "api://devices",
70
+ device: "api://devices/{{kind}}",
71
71
  keychain: "api://keys/{{kind}}",
72
72
  udt: "api://data-structures",
73
73
  scenario: "api://scenario-list"
@@ -310,16 +310,17 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
310
310
  case "select":
311
311
  case "account":
312
312
  case "hook":
313
+ case "device":
313
314
  case "keychain":
314
315
  case "datastore":
315
316
  case "aiagent":
316
317
  case "file":
317
318
  case "folder":
318
- return handleSelectType(normalizedField, result, context);
319
+ return handleSelectOrPathType(normalizedField, result, context);
319
320
  case "filter":
320
321
  return handleFilterType(normalizedField, result, context);
321
322
  default:
322
- return handlePrimitiveType(normalizedField, result);
323
+ return handlePrimitiveType(normalizedField, result, context);
323
324
  }
324
325
  }
325
326
  function handleCollectionType(field, result, context) {
@@ -435,8 +436,22 @@ function handleFilterType(field, result, context) {
435
436
  });
436
437
  return result;
437
438
  }
438
- function handleSelectType(field, result, context) {
439
+ function handleSelectOrPathType(field, result, context) {
439
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
+ }
440
455
  const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
441
456
  const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
442
457
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
@@ -545,9 +560,10 @@ function handleSelectType(field, result, context) {
545
560
  )
546
561
  });
547
562
  }
563
+ if (field.rpc) result = processRpcDirective(field, result, context);
548
564
  return result;
549
565
  }
550
- function handlePrimitiveType(field, result) {
566
+ function handlePrimitiveType(field, result, context) {
551
567
  if (field.default !== "" && field.default != null) {
552
568
  result.default = field.default;
553
569
  }
@@ -565,6 +581,21 @@ function handlePrimitiveType(field, result) {
565
581
  result.enum = field.validate.enum;
566
582
  }
567
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
+ });
568
599
  return result;
569
600
  }
570
601
 
@@ -572,6 +603,7 @@ function handlePrimitiveType(field, result) {
572
603
  var FORMAN_TYPE_MAP2 = {
573
604
  account: "number",
574
605
  hook: "number",
606
+ device: "number",
575
607
  keychain: "number",
576
608
  datastore: "number",
577
609
  aiagent: "string",
@@ -652,7 +684,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
652
684
  validateNestedFields: () => {
653
685
  throw new Error("Cannot validate nested fields without parent field.");
654
686
  },
655
- resolveRemote: async (path, context) => {
687
+ resolveRemote: async (path, context, localData) => {
656
688
  if (!options?.resolveRemote) {
657
689
  throw new Error("Remote resource not supported when resolver is not provided.");
658
690
  }
@@ -663,7 +695,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
663
695
  },
664
696
  {}
665
697
  );
666
- return await options.resolveRemote(path, data);
698
+ return await options.resolveRemote(path, {
699
+ ...data,
700
+ ...localData
701
+ });
667
702
  }
668
703
  }
669
704
  );
@@ -750,9 +785,10 @@ async function validateFormanValue(value, field, context) {
750
785
  case "aiagent":
751
786
  case "udt":
752
787
  case "scenario":
788
+ return handleSelectType(value, normalizedField, context);
753
789
  case "file":
754
790
  case "folder":
755
- return handleSelectType2(value, normalizedField, context);
791
+ return handlePathType(value, normalizedField, context);
756
792
  case "filter":
757
793
  return handleFilterType2(value, normalizedField, context);
758
794
  default:
@@ -917,7 +953,152 @@ async function handleFilterType2(value, field, context) {
917
953
  };
918
954
  return handleArrayType2(value, inlineSchema, context);
919
955
  }
920
- 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) {
921
1102
  const errors = [];
922
1103
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
923
1104
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -1189,6 +1370,19 @@ function toFormanSchema(field) {
1189
1370
  }
1190
1371
  return formanSchema;
1191
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
+ }
1192
1386
  if (field.enum) {
1193
1387
  return {
1194
1388
  type: "select",
@@ -1204,7 +1398,7 @@ function toFormanSchema(field) {
1204
1398
  options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1205
1399
  };
1206
1400
  }
1207
- const textField = {
1401
+ let textField = {
1208
1402
  type: "text",
1209
1403
  label: noEmpty(field.title),
1210
1404
  help: noEmpty(field.description),
@@ -1215,9 +1409,12 @@ function toFormanSchema(field) {
1215
1409
  if (field.pattern) textField.validate.pattern = field.pattern;
1216
1410
  if (field.enum) textField.validate.enum = field.enum;
1217
1411
  }
1412
+ if ("x-search" in field) {
1413
+ textField = handleSearchDirective(textField, field["x-search"]);
1414
+ }
1218
1415
  return textField;
1219
1416
  default:
1220
- const primitiveField = {
1417
+ let primitiveField = {
1221
1418
  type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
1222
1419
  label: noEmpty(field.title),
1223
1420
  help: noEmpty(field.description),
@@ -1228,9 +1425,20 @@ function toFormanSchema(field) {
1228
1425
  if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
1229
1426
  if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
1230
1427
  }
1428
+ if ("x-search" in field) {
1429
+ primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
1430
+ }
1231
1431
  return primitiveField;
1232
1432
  }
1233
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
+ }
1234
1442
 
1235
1443
  // src/index.ts
1236
1444
  function toJSONSchema(field) {
package/dist/index.d.cts CHANGED
@@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema';
3
3
  /**
4
4
  * Valid Forman Schema field types
5
5
  */
6
- type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
6
+ type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | `device:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
7
7
  /**
8
8
  * Validation configuration for Forman Schema fields
9
9
  */
@@ -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
@@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema';
3
3
  /**
4
4
  * Valid Forman Schema field types
5
5
  */
6
- type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
6
+ type FormanSchemaFieldType = 'aiagent' | 'account' | 'hook' | 'device' | 'keychain' | 'datastore' | 'udt' | 'scenario' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'filter' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | `device:${string}` | 'banner' | 'markdown' | 'html' | 'separator' | string;
7
7
  /**
8
8
  * Validation configuration for Forman Schema fields
9
9
  */
@@ -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
@@ -38,7 +38,7 @@ var API_ENDPOINTS = {
38
38
  aiagent: "api://ai-agents/v1/agents",
39
39
  datastore: "api://data-stores",
40
40
  hook: "api://hooks/{{kind}}",
41
- device: "api://devices",
41
+ device: "api://devices/{{kind}}",
42
42
  keychain: "api://keys/{{kind}}",
43
43
  udt: "api://data-structures",
44
44
  scenario: "api://scenario-list"
@@ -281,16 +281,17 @@ function toJSONSchemaInternal(field, context = createDefaultContext()) {
281
281
  case "select":
282
282
  case "account":
283
283
  case "hook":
284
+ case "device":
284
285
  case "keychain":
285
286
  case "datastore":
286
287
  case "aiagent":
287
288
  case "file":
288
289
  case "folder":
289
- return handleSelectType(normalizedField, result, context);
290
+ return handleSelectOrPathType(normalizedField, result, context);
290
291
  case "filter":
291
292
  return handleFilterType(normalizedField, result, context);
292
293
  default:
293
- return handlePrimitiveType(normalizedField, result);
294
+ return handlePrimitiveType(normalizedField, result, context);
294
295
  }
295
296
  }
296
297
  function handleCollectionType(field, result, context) {
@@ -406,8 +407,22 @@ function handleFilterType(field, result, context) {
406
407
  });
407
408
  return result;
408
409
  }
409
- function handleSelectType(field, result, context) {
410
+ function handleSelectOrPathType(field, result, context) {
410
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
+ }
411
426
  const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
412
427
  const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
413
428
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
@@ -516,9 +531,10 @@ function handleSelectType(field, result, context) {
516
531
  )
517
532
  });
518
533
  }
534
+ if (field.rpc) result = processRpcDirective(field, result, context);
519
535
  return result;
520
536
  }
521
- function handlePrimitiveType(field, result) {
537
+ function handlePrimitiveType(field, result, context) {
522
538
  if (field.default !== "" && field.default != null) {
523
539
  result.default = field.default;
524
540
  }
@@ -536,6 +552,21 @@ function handlePrimitiveType(field, result) {
536
552
  result.enum = field.validate.enum;
537
553
  }
538
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
+ });
539
570
  return result;
540
571
  }
541
572
 
@@ -543,6 +574,7 @@ function handlePrimitiveType(field, result) {
543
574
  var FORMAN_TYPE_MAP2 = {
544
575
  account: "number",
545
576
  hook: "number",
577
+ device: "number",
546
578
  keychain: "number",
547
579
  datastore: "number",
548
580
  aiagent: "string",
@@ -623,7 +655,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
623
655
  validateNestedFields: () => {
624
656
  throw new Error("Cannot validate nested fields without parent field.");
625
657
  },
626
- resolveRemote: async (path, context) => {
658
+ resolveRemote: async (path, context, localData) => {
627
659
  if (!options?.resolveRemote) {
628
660
  throw new Error("Remote resource not supported when resolver is not provided.");
629
661
  }
@@ -634,7 +666,10 @@ async function validateFormanWithDomainsInternal(domains, options) {
634
666
  },
635
667
  {}
636
668
  );
637
- return await options.resolveRemote(path, data);
669
+ return await options.resolveRemote(path, {
670
+ ...data,
671
+ ...localData
672
+ });
638
673
  }
639
674
  }
640
675
  );
@@ -721,9 +756,10 @@ async function validateFormanValue(value, field, context) {
721
756
  case "aiagent":
722
757
  case "udt":
723
758
  case "scenario":
759
+ return handleSelectType(value, normalizedField, context);
724
760
  case "file":
725
761
  case "folder":
726
- return handleSelectType2(value, normalizedField, context);
762
+ return handlePathType(value, normalizedField, context);
727
763
  case "filter":
728
764
  return handleFilterType2(value, normalizedField, context);
729
765
  default:
@@ -888,7 +924,152 @@ async function handleFilterType2(value, field, context) {
888
924
  };
889
925
  return handleArrayType2(value, inlineSchema, context);
890
926
  }
891
- 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) {
892
1073
  const errors = [];
893
1074
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
894
1075
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -1160,6 +1341,19 @@ function toFormanSchema(field) {
1160
1341
  }
1161
1342
  return formanSchema;
1162
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
+ }
1163
1357
  if (field.enum) {
1164
1358
  return {
1165
1359
  type: "select",
@@ -1175,7 +1369,7 @@ function toFormanSchema(field) {
1175
1369
  options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1176
1370
  };
1177
1371
  }
1178
- const textField = {
1372
+ let textField = {
1179
1373
  type: "text",
1180
1374
  label: noEmpty(field.title),
1181
1375
  help: noEmpty(field.description),
@@ -1186,9 +1380,12 @@ function toFormanSchema(field) {
1186
1380
  if (field.pattern) textField.validate.pattern = field.pattern;
1187
1381
  if (field.enum) textField.validate.enum = field.enum;
1188
1382
  }
1383
+ if ("x-search" in field) {
1384
+ textField = handleSearchDirective(textField, field["x-search"]);
1385
+ }
1189
1386
  return textField;
1190
1387
  default:
1191
- const primitiveField = {
1388
+ let primitiveField = {
1192
1389
  type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "any",
1193
1390
  label: noEmpty(field.title),
1194
1391
  help: noEmpty(field.description),
@@ -1199,9 +1396,20 @@ function toFormanSchema(field) {
1199
1396
  if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
1200
1397
  if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
1201
1398
  }
1399
+ if ("x-search" in field) {
1400
+ primitiveField = handleSearchDirective(primitiveField, field["x-search"]);
1401
+ }
1202
1402
  return primitiveField;
1203
1403
  }
1204
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
+ }
1205
1413
 
1206
1414
  // src/index.ts
1207
1415
  function toJSONSchema(field) {
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.7.0",
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",