@makehq/forman-schema 1.4.1 → 1.5.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
@@ -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,69 @@ 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
+ }
74
155
 
75
156
  // src/forman.ts
76
157
  var SchemaConversionError = class extends Error {
@@ -182,7 +263,7 @@ function handleCollectionType(field, result, context) {
182
263
  required: []
183
264
  });
184
265
  function addField(subField, tail) {
185
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
266
+ if (isVisualType(subField.type)) {
186
267
  return;
187
268
  }
188
269
  if (!subField.name) return;
@@ -427,6 +508,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
427
508
  (acc, domain) => {
428
509
  acc[domain] = {
429
510
  seenFields: /* @__PURE__ */ new Set(),
511
+ fieldStates: [],
430
512
  validateFields: (fields, context) => {
431
513
  return validateFormanValue(
432
514
  domains[domain].values,
@@ -484,11 +566,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
484
566
  }
485
567
  return {
486
568
  valid: errors.length === 0,
487
- errors
569
+ errors,
570
+ states: errors.length === 0 && options?.states ? buildRestoreStructure(
571
+ Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
572
+ ) : void 0
488
573
  };
489
574
  }
490
575
  async function validateFormanValue(value, field, context) {
491
- if (FORMAN_VISUAL_TYPES.includes(field.type)) {
576
+ if (isVisualType(field.type)) {
492
577
  return {
493
578
  valid: true,
494
579
  errors: []
@@ -528,6 +613,24 @@ async function validateFormanValue(value, field, context) {
528
613
  ]
529
614
  };
530
615
  }
616
+ if (containsIMLExpression(value)) {
617
+ if (normalizedField.mappable === false) {
618
+ return {
619
+ valid: false,
620
+ errors: [
621
+ {
622
+ domain: context.domain,
623
+ path: context.path.join("."),
624
+ message: "Value contains prohibited IML expression."
625
+ }
626
+ ]
627
+ };
628
+ }
629
+ return {
630
+ valid: true,
631
+ errors: []
632
+ };
633
+ }
531
634
  switch (normalizedField.type) {
532
635
  case "collection":
533
636
  return handleCollectionType2(value, normalizedField, context);
@@ -536,10 +639,12 @@ async function validateFormanValue(value, field, context) {
536
639
  case "select":
537
640
  case "account":
538
641
  case "hook":
642
+ case "device":
539
643
  case "keychain":
540
644
  case "datastore":
541
645
  case "aiagent":
542
646
  case "udt":
647
+ case "scenario":
543
648
  case "file":
544
649
  case "folder":
545
650
  return handleSelectType2(value, normalizedField, context);
@@ -579,7 +684,7 @@ async function handleCollectionType2(value, field, context) {
579
684
  };
580
685
  }
581
686
  }
582
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
687
+ if (isVisualType(subField.type)) {
583
688
  continue;
584
689
  }
585
690
  if (!subField.name) {
@@ -596,7 +701,7 @@ async function handleCollectionType2(value, field, context) {
596
701
  path: [...path, subField.name],
597
702
  validateNestedFields: async (fields, context2) => {
598
703
  for (const subField2 of fields) {
599
- if (FORMAN_VISUAL_TYPES.includes(subField2.type)) {
704
+ if (isVisualType(subField2.type)) {
600
705
  continue;
601
706
  }
602
707
  if (!subField2.name) {
@@ -643,7 +748,7 @@ async function handleArrayType2(value, field, context) {
643
748
  const result = await validateFormanValue(
644
749
  item,
645
750
  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()] }
751
+ { ...context, path: [...context.path, index] }
647
752
  );
648
753
  errors.push(...result.errors);
649
754
  }
@@ -747,6 +852,15 @@ async function handleSelectType2(value, field, context) {
747
852
  ]
748
853
  };
749
854
  }
855
+ if (item.label) {
856
+ context.roots[context.domain].fieldStates.push({
857
+ path: context.path,
858
+ state: {
859
+ mode: isReferenceType(field.type) ? void 0 : "chose",
860
+ label: item.label
861
+ }
862
+ });
863
+ }
750
864
  if (item.nested) nested = item.nested;
751
865
  }
752
866
  if (nested) {
@@ -815,18 +929,6 @@ async function handleNestedFields(nested, value, field, context) {
815
929
  }
816
930
  async function handlePrimitiveType2(value, field, context) {
817
931
  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
932
  if (field.validate) {
831
933
  if (typeof value === "string") {
832
934
  if (field.validate.pattern && !new RegExp(
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' | '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
  */
@@ -163,10 +163,21 @@ type FormanValidationResult = {
163
163
  /** Error message */
164
164
  message: string;
165
165
  }[];
166
+ /** States of fields grouped by domain */
167
+ states?: Record<string, FormanSchemaFieldState>;
168
+ };
169
+ type FormanSchemaFieldState = {
170
+ mode?: 'chose' | 'edit';
171
+ label?: string;
172
+ data?: Record<string, unknown>;
173
+ nested?: Record<string, FormanSchemaFieldState>;
174
+ items?: Record<string, FormanSchemaFieldState>[];
166
175
  };
167
176
  type FormanValidationOptions = {
168
177
  /** Unknown fields are not allowed when strict is true */
169
178
  strict?: boolean;
179
+ /** Whether to generate states for fields */
180
+ states?: boolean;
170
181
  /** Remote resource resolver */
171
182
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
172
183
  };
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' | '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
  */
@@ -163,10 +163,21 @@ type FormanValidationResult = {
163
163
  /** Error message */
164
164
  message: string;
165
165
  }[];
166
+ /** States of fields grouped by domain */
167
+ states?: Record<string, FormanSchemaFieldState>;
168
+ };
169
+ type FormanSchemaFieldState = {
170
+ mode?: 'chose' | 'edit';
171
+ label?: string;
172
+ data?: Record<string, unknown>;
173
+ nested?: Record<string, FormanSchemaFieldState>;
174
+ items?: Record<string, FormanSchemaFieldState>[];
166
175
  };
167
176
  type FormanValidationOptions = {
168
177
  /** Unknown fields are not allowed when strict is true */
169
178
  strict?: boolean;
179
+ /** Whether to generate states for fields */
180
+ states?: boolean;
170
181
  /** Remote resource resolver */
171
182
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
172
183
  };
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,69 @@ 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
+ }
45
126
 
46
127
  // src/forman.ts
47
128
  var SchemaConversionError = class extends Error {
@@ -153,7 +234,7 @@ function handleCollectionType(field, result, context) {
153
234
  required: []
154
235
  });
155
236
  function addField(subField, tail) {
156
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
237
+ if (isVisualType(subField.type)) {
157
238
  return;
158
239
  }
159
240
  if (!subField.name) return;
@@ -398,6 +479,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
398
479
  (acc, domain) => {
399
480
  acc[domain] = {
400
481
  seenFields: /* @__PURE__ */ new Set(),
482
+ fieldStates: [],
401
483
  validateFields: (fields, context) => {
402
484
  return validateFormanValue(
403
485
  domains[domain].values,
@@ -455,11 +537,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
455
537
  }
456
538
  return {
457
539
  valid: errors.length === 0,
458
- errors
540
+ errors,
541
+ states: errors.length === 0 && options?.states ? buildRestoreStructure(
542
+ Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
543
+ ) : void 0
459
544
  };
460
545
  }
461
546
  async function validateFormanValue(value, field, context) {
462
- if (FORMAN_VISUAL_TYPES.includes(field.type)) {
547
+ if (isVisualType(field.type)) {
463
548
  return {
464
549
  valid: true,
465
550
  errors: []
@@ -499,6 +584,24 @@ async function validateFormanValue(value, field, context) {
499
584
  ]
500
585
  };
501
586
  }
587
+ if (containsIMLExpression(value)) {
588
+ if (normalizedField.mappable === false) {
589
+ return {
590
+ valid: false,
591
+ errors: [
592
+ {
593
+ domain: context.domain,
594
+ path: context.path.join("."),
595
+ message: "Value contains prohibited IML expression."
596
+ }
597
+ ]
598
+ };
599
+ }
600
+ return {
601
+ valid: true,
602
+ errors: []
603
+ };
604
+ }
502
605
  switch (normalizedField.type) {
503
606
  case "collection":
504
607
  return handleCollectionType2(value, normalizedField, context);
@@ -507,10 +610,12 @@ async function validateFormanValue(value, field, context) {
507
610
  case "select":
508
611
  case "account":
509
612
  case "hook":
613
+ case "device":
510
614
  case "keychain":
511
615
  case "datastore":
512
616
  case "aiagent":
513
617
  case "udt":
618
+ case "scenario":
514
619
  case "file":
515
620
  case "folder":
516
621
  return handleSelectType2(value, normalizedField, context);
@@ -550,7 +655,7 @@ async function handleCollectionType2(value, field, context) {
550
655
  };
551
656
  }
552
657
  }
553
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
658
+ if (isVisualType(subField.type)) {
554
659
  continue;
555
660
  }
556
661
  if (!subField.name) {
@@ -567,7 +672,7 @@ async function handleCollectionType2(value, field, context) {
567
672
  path: [...path, subField.name],
568
673
  validateNestedFields: async (fields, context2) => {
569
674
  for (const subField2 of fields) {
570
- if (FORMAN_VISUAL_TYPES.includes(subField2.type)) {
675
+ if (isVisualType(subField2.type)) {
571
676
  continue;
572
677
  }
573
678
  if (!subField2.name) {
@@ -614,7 +719,7 @@ async function handleArrayType2(value, field, context) {
614
719
  const result = await validateFormanValue(
615
720
  item,
616
721
  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()] }
722
+ { ...context, path: [...context.path, index] }
618
723
  );
619
724
  errors.push(...result.errors);
620
725
  }
@@ -718,6 +823,15 @@ async function handleSelectType2(value, field, context) {
718
823
  ]
719
824
  };
720
825
  }
826
+ if (item.label) {
827
+ context.roots[context.domain].fieldStates.push({
828
+ path: context.path,
829
+ state: {
830
+ mode: isReferenceType(field.type) ? void 0 : "chose",
831
+ label: item.label
832
+ }
833
+ });
834
+ }
721
835
  if (item.nested) nested = item.nested;
722
836
  }
723
837
  if (nested) {
@@ -786,18 +900,6 @@ async function handleNestedFields(nested, value, field, context) {
786
900
  }
787
901
  async function handlePrimitiveType2(value, field, context) {
788
902
  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
903
  if (field.validate) {
802
904
  if (typeof value === "string") {
803
905
  if (field.validate.pattern && !new RegExp(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",