@makehq/forman-schema 1.4.1 → 1.6.0

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