@makehq/forman-schema 1.4.0 → 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
  }
@@ -42,13 +58,19 @@ function containsIMLExpression(value) {
42
58
  if (typeof value !== "string") return false;
43
59
  return value.indexOf("{{") > -1 && value.indexOf("}}") > -1;
44
60
  }
61
+ function isPrimitiveIMLExpression(value) {
62
+ if (typeof value !== "string") return false;
63
+ return value.lastIndexOf("{{") === 0 && value.indexOf("}}") === value.length - 2;
64
+ }
45
65
  var API_ENDPOINTS = {
46
66
  account: "api://connections/{{kind}}",
47
67
  aiagent: "api://ai-agents/v1/agents",
48
68
  datastore: "api://data-stores",
49
69
  hook: "api://hooks/{{kind}}",
70
+ device: "api://devices",
50
71
  keychain: "api://keys/{{kind}}",
51
- udt: "api://data-structures"
72
+ udt: "api://data-structures",
73
+ scenario: "api://scenario-list"
52
74
  };
53
75
  function normalizeFormanFieldType(field) {
54
76
  const [type, kind] = field.type.split(":");
@@ -67,6 +89,69 @@ function normalizeFormanFieldType(field) {
67
89
  }
68
90
  };
69
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
+ }
70
155
 
71
156
  // src/forman.ts
72
157
  var SchemaConversionError = class extends Error {
@@ -178,7 +263,7 @@ function handleCollectionType(field, result, context) {
178
263
  required: []
179
264
  });
180
265
  function addField(subField, tail) {
181
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
266
+ if (isVisualType(subField.type)) {
182
267
  return;
183
268
  }
184
269
  if (!subField.name) return;
@@ -423,6 +508,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
423
508
  (acc, domain) => {
424
509
  acc[domain] = {
425
510
  seenFields: /* @__PURE__ */ new Set(),
511
+ fieldStates: [],
426
512
  validateFields: (fields, context) => {
427
513
  return validateFormanValue(
428
514
  domains[domain].values,
@@ -480,11 +566,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
480
566
  }
481
567
  return {
482
568
  valid: errors.length === 0,
483
- 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
484
573
  };
485
574
  }
486
575
  async function validateFormanValue(value, field, context) {
487
- if (FORMAN_VISUAL_TYPES.includes(field.type)) {
576
+ if (isVisualType(field.type)) {
488
577
  return {
489
578
  valid: true,
490
579
  errors: []
@@ -512,7 +601,7 @@ async function validateFormanValue(value, field, context) {
512
601
  const expectedType = FORMAN_TYPE_MAP2[normalizedField.type];
513
602
  let actualType = typeof value;
514
603
  if (actualType === "object" && Array.isArray(value)) actualType = "array";
515
- if (expectedType && expectedType !== actualType) {
604
+ if (expectedType && expectedType !== actualType && !isPrimitiveIMLExpression(value)) {
516
605
  return {
517
606
  valid: false,
518
607
  errors: [
@@ -524,6 +613,24 @@ async function validateFormanValue(value, field, context) {
524
613
  ]
525
614
  };
526
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
+ }
527
634
  switch (normalizedField.type) {
528
635
  case "collection":
529
636
  return handleCollectionType2(value, normalizedField, context);
@@ -532,10 +639,12 @@ async function validateFormanValue(value, field, context) {
532
639
  case "select":
533
640
  case "account":
534
641
  case "hook":
642
+ case "device":
535
643
  case "keychain":
536
644
  case "datastore":
537
645
  case "aiagent":
538
646
  case "udt":
647
+ case "scenario":
539
648
  case "file":
540
649
  case "folder":
541
650
  return handleSelectType2(value, normalizedField, context);
@@ -575,7 +684,7 @@ async function handleCollectionType2(value, field, context) {
575
684
  };
576
685
  }
577
686
  }
578
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
687
+ if (isVisualType(subField.type)) {
579
688
  continue;
580
689
  }
581
690
  if (!subField.name) {
@@ -592,7 +701,7 @@ async function handleCollectionType2(value, field, context) {
592
701
  path: [...path, subField.name],
593
702
  validateNestedFields: async (fields, context2) => {
594
703
  for (const subField2 of fields) {
595
- if (FORMAN_VISUAL_TYPES.includes(subField2.type)) {
704
+ if (isVisualType(subField2.type)) {
596
705
  continue;
597
706
  }
598
707
  if (!subField2.name) {
@@ -639,7 +748,7 @@ async function handleArrayType2(value, field, context) {
639
748
  const result = await validateFormanValue(
640
749
  item,
641
750
  Array.isArray(field.spec) ? { name: index.toString(), type: "collection", spec: field.spec } : Object.assign({}, field.spec, { name: index.toString() }),
642
- { ...context, path: [...context.path, index.toString()] }
751
+ { ...context, path: [...context.path, index] }
643
752
  );
644
753
  errors.push(...result.errors);
645
754
  }
@@ -743,6 +852,15 @@ async function handleSelectType2(value, field, context) {
743
852
  ]
744
853
  };
745
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
+ }
746
864
  if (item.nested) nested = item.nested;
747
865
  }
748
866
  if (nested) {
@@ -811,18 +929,6 @@ async function handleNestedFields(nested, value, field, context) {
811
929
  }
812
930
  async function handlePrimitiveType2(value, field, context) {
813
931
  const errors = [];
814
- if (containsIMLExpression(value)) {
815
- return {
816
- valid: true,
817
- errors
818
- };
819
- }
820
- if (errors.length > 0) {
821
- return {
822
- valid: false,
823
- errors
824
- };
825
- }
826
932
  if (field.validate) {
827
933
  if (typeof value === "string") {
828
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
  }
@@ -13,13 +29,19 @@ function containsIMLExpression(value) {
13
29
  if (typeof value !== "string") return false;
14
30
  return value.indexOf("{{") > -1 && value.indexOf("}}") > -1;
15
31
  }
32
+ function isPrimitiveIMLExpression(value) {
33
+ if (typeof value !== "string") return false;
34
+ return value.lastIndexOf("{{") === 0 && value.indexOf("}}") === value.length - 2;
35
+ }
16
36
  var API_ENDPOINTS = {
17
37
  account: "api://connections/{{kind}}",
18
38
  aiagent: "api://ai-agents/v1/agents",
19
39
  datastore: "api://data-stores",
20
40
  hook: "api://hooks/{{kind}}",
41
+ device: "api://devices",
21
42
  keychain: "api://keys/{{kind}}",
22
- udt: "api://data-structures"
43
+ udt: "api://data-structures",
44
+ scenario: "api://scenario-list"
23
45
  };
24
46
  function normalizeFormanFieldType(field) {
25
47
  const [type, kind] = field.type.split(":");
@@ -38,6 +60,69 @@ function normalizeFormanFieldType(field) {
38
60
  }
39
61
  };
40
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
+ }
41
126
 
42
127
  // src/forman.ts
43
128
  var SchemaConversionError = class extends Error {
@@ -149,7 +234,7 @@ function handleCollectionType(field, result, context) {
149
234
  required: []
150
235
  });
151
236
  function addField(subField, tail) {
152
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
237
+ if (isVisualType(subField.type)) {
153
238
  return;
154
239
  }
155
240
  if (!subField.name) return;
@@ -394,6 +479,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
394
479
  (acc, domain) => {
395
480
  acc[domain] = {
396
481
  seenFields: /* @__PURE__ */ new Set(),
482
+ fieldStates: [],
397
483
  validateFields: (fields, context) => {
398
484
  return validateFormanValue(
399
485
  domains[domain].values,
@@ -451,11 +537,14 @@ async function validateFormanWithDomainsInternal(domains, options) {
451
537
  }
452
538
  return {
453
539
  valid: errors.length === 0,
454
- 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
455
544
  };
456
545
  }
457
546
  async function validateFormanValue(value, field, context) {
458
- if (FORMAN_VISUAL_TYPES.includes(field.type)) {
547
+ if (isVisualType(field.type)) {
459
548
  return {
460
549
  valid: true,
461
550
  errors: []
@@ -483,7 +572,7 @@ async function validateFormanValue(value, field, context) {
483
572
  const expectedType = FORMAN_TYPE_MAP2[normalizedField.type];
484
573
  let actualType = typeof value;
485
574
  if (actualType === "object" && Array.isArray(value)) actualType = "array";
486
- if (expectedType && expectedType !== actualType) {
575
+ if (expectedType && expectedType !== actualType && !isPrimitiveIMLExpression(value)) {
487
576
  return {
488
577
  valid: false,
489
578
  errors: [
@@ -495,6 +584,24 @@ async function validateFormanValue(value, field, context) {
495
584
  ]
496
585
  };
497
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
+ }
498
605
  switch (normalizedField.type) {
499
606
  case "collection":
500
607
  return handleCollectionType2(value, normalizedField, context);
@@ -503,10 +610,12 @@ async function validateFormanValue(value, field, context) {
503
610
  case "select":
504
611
  case "account":
505
612
  case "hook":
613
+ case "device":
506
614
  case "keychain":
507
615
  case "datastore":
508
616
  case "aiagent":
509
617
  case "udt":
618
+ case "scenario":
510
619
  case "file":
511
620
  case "folder":
512
621
  return handleSelectType2(value, normalizedField, context);
@@ -546,7 +655,7 @@ async function handleCollectionType2(value, field, context) {
546
655
  };
547
656
  }
548
657
  }
549
- if (FORMAN_VISUAL_TYPES.includes(subField.type)) {
658
+ if (isVisualType(subField.type)) {
550
659
  continue;
551
660
  }
552
661
  if (!subField.name) {
@@ -563,7 +672,7 @@ async function handleCollectionType2(value, field, context) {
563
672
  path: [...path, subField.name],
564
673
  validateNestedFields: async (fields, context2) => {
565
674
  for (const subField2 of fields) {
566
- if (FORMAN_VISUAL_TYPES.includes(subField2.type)) {
675
+ if (isVisualType(subField2.type)) {
567
676
  continue;
568
677
  }
569
678
  if (!subField2.name) {
@@ -610,7 +719,7 @@ async function handleArrayType2(value, field, context) {
610
719
  const result = await validateFormanValue(
611
720
  item,
612
721
  Array.isArray(field.spec) ? { name: index.toString(), type: "collection", spec: field.spec } : Object.assign({}, field.spec, { name: index.toString() }),
613
- { ...context, path: [...context.path, index.toString()] }
722
+ { ...context, path: [...context.path, index] }
614
723
  );
615
724
  errors.push(...result.errors);
616
725
  }
@@ -714,6 +823,15 @@ async function handleSelectType2(value, field, context) {
714
823
  ]
715
824
  };
716
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
+ }
717
835
  if (item.nested) nested = item.nested;
718
836
  }
719
837
  if (nested) {
@@ -782,18 +900,6 @@ async function handleNestedFields(nested, value, field, context) {
782
900
  }
783
901
  async function handlePrimitiveType2(value, field, context) {
784
902
  const errors = [];
785
- if (containsIMLExpression(value)) {
786
- return {
787
- valid: true,
788
- errors
789
- };
790
- }
791
- if (errors.length > 0) {
792
- return {
793
- valid: false,
794
- errors
795
- };
796
- }
797
903
  if (field.validate) {
798
904
  if (typeof value === "string") {
799
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.0",
3
+ "version": "1.5.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",