@makehq/forman-schema 1.8.4 → 1.9.1

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
@@ -98,6 +98,60 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
98
98
  const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
99
99
  return found;
100
100
  }
101
+ function pathToString(path) {
102
+ let result = "";
103
+ for (const key of path) {
104
+ if (typeof key === "number") {
105
+ result += `[${key}]`;
106
+ } else {
107
+ if (result.length > 0) {
108
+ result += ".";
109
+ }
110
+ if (key.includes("`")) {
111
+ throw new Error(`Invalid path key: backticks are not allowed in key "${key}"`);
112
+ }
113
+ result += key.includes(".") ? `\`${key}\`` : key;
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ function stringToPath(str) {
119
+ const path = [];
120
+ let i = 0;
121
+ while (i < str.length) {
122
+ if (str[i] === ".") {
123
+ i++;
124
+ if (i >= str.length) break;
125
+ }
126
+ if (str[i] === "[") {
127
+ const end = str.indexOf("]", i);
128
+ if (end === -1) {
129
+ throw new Error(`Invalid path: missing closing bracket in "${str}"`);
130
+ }
131
+ const num = Number(str.slice(i + 1, end));
132
+ if (!Number.isInteger(num) || num < 0) {
133
+ throw new Error(`Invalid path: non-numeric or negative index in "${str}"`);
134
+ }
135
+ path.push(num);
136
+ i = end + 1;
137
+ } else if (str[i] === "`") {
138
+ const end = str.indexOf("`", i + 1);
139
+ if (end === -1) {
140
+ throw new Error(`Invalid path: missing closing backtick in "${str}"`);
141
+ }
142
+ path.push(str.slice(i + 1, end));
143
+ i = end + 1;
144
+ } else {
145
+ let end = i;
146
+ while (end < str.length && str[end] !== "." && str[end] !== "[") {
147
+ end++;
148
+ }
149
+ path.push(str.slice(i, end));
150
+ i = end;
151
+ }
152
+ }
153
+ return path;
154
+ }
101
155
  function buildRestoreStructure(items) {
102
156
  const result = {};
103
157
  for (const item of items) {
@@ -531,7 +585,8 @@ function validateFormanField(field) {
531
585
  }
532
586
  function appendQueryString(path, domain, tail) {
533
587
  if (path.startsWith("api://")) return path;
534
- const queryString = tail.map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
588
+ const existingParams = new Set(new URL(path).searchParams.keys());
589
+ const queryString = tail.filter((part) => !existingParams.has(part)).map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
535
590
  if (!queryString) return path;
536
591
  const separator = path.includes("?") ? "&" : "?";
537
592
  return `${path}${separator}${queryString}`;
@@ -1079,6 +1134,25 @@ async function validateFormanWithDomainsInternal(domains, options) {
1079
1134
  }
1080
1135
  );
1081
1136
  errors.push(...result.errors);
1137
+ const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1138
+ if (restoreExtras) {
1139
+ const fieldStateMap = new Map(
1140
+ roots[domain].fieldStates.map(({ path, state }) => {
1141
+ return [pathToString(path), state];
1142
+ })
1143
+ );
1144
+ for (const [fieldPath, extra] of Object.entries(restoreExtras)) {
1145
+ const state = fieldStateMap.get(fieldPath);
1146
+ if (state) {
1147
+ state.extra = extra;
1148
+ } else {
1149
+ roots[domain].fieldStates.push({
1150
+ path: stringToPath(fieldPath),
1151
+ state: { extra }
1152
+ });
1153
+ }
1154
+ }
1155
+ }
1082
1156
  }
1083
1157
  return {
1084
1158
  valid: errors.length === 0,
@@ -1506,11 +1580,14 @@ async function handleSelectType(value, field, context) {
1506
1580
  if (resolved == null || typeof resolved !== "object") {
1507
1581
  return {
1508
1582
  valid: false,
1509
- errors: [...errors, {
1510
- domain: context.domain,
1511
- path: context.path.join("."),
1512
- message: `Remote resource ${optionsOrGroups} returned no data.`
1513
- }]
1583
+ errors: [
1584
+ ...errors,
1585
+ {
1586
+ domain: context.domain,
1587
+ path: context.path.join("."),
1588
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1589
+ }
1590
+ ]
1514
1591
  };
1515
1592
  }
1516
1593
  optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
@@ -1876,8 +1953,8 @@ function toJSONSchema(field) {
1876
1953
  function validateFormanWithDomains(domains, options) {
1877
1954
  return validateFormanWithDomainsInternal(domains, options);
1878
1955
  }
1879
- function validateForman(values, schema, options) {
1880
- return validateFormanWithDomains({ default: { values, schema } }, options);
1956
+ function validateForman(values, schema, options, restoreExtras) {
1957
+ return validateFormanWithDomains({ default: { values, schema, restoreExtras } }, options);
1881
1958
  }
1882
1959
  // Annotate the CommonJS export names for ESM import in node:
1883
1960
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -205,6 +205,7 @@ type FormanSchemaFieldState = {
205
205
  label?: string;
206
206
  path?: Array<string>;
207
207
  data?: Record<string, unknown>;
208
+ extra?: Record<string, unknown>;
208
209
  nested?: Record<string, FormanSchemaFieldState>;
209
210
  items?: Record<string, FormanSchemaFieldState>[];
210
211
  };
@@ -239,14 +240,19 @@ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
239
240
  declare function validateFormanWithDomains(domains: Record<string, {
240
241
  values: Record<string, unknown>;
241
242
  schema: FormanSchemaField[];
243
+ /** Extra values injected into restore states, keyed by string path (dot notation, `[index]`, backtick-escaping). */
244
+ restoreExtras?: Record<string, Record<string, unknown>>;
242
245
  }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
243
246
  /**
244
247
  * Validates a simple Forman values against a schema
245
248
  * @param values The values to validate
246
249
  * @param schema The schema to validate against
247
250
  * @param options The validation options
251
+ * @param restoreExtras Values to be injected into restore objects of particular fields.
252
+ * Keyed by string path using dot notation for nested keys, `[index]` for array indices,
253
+ * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
248
254
  * @returns The validation result
249
255
  */
250
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
256
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
251
257
 
252
258
  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
@@ -205,6 +205,7 @@ type FormanSchemaFieldState = {
205
205
  label?: string;
206
206
  path?: Array<string>;
207
207
  data?: Record<string, unknown>;
208
+ extra?: Record<string, unknown>;
208
209
  nested?: Record<string, FormanSchemaFieldState>;
209
210
  items?: Record<string, FormanSchemaFieldState>[];
210
211
  };
@@ -239,14 +240,19 @@ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
239
240
  declare function validateFormanWithDomains(domains: Record<string, {
240
241
  values: Record<string, unknown>;
241
242
  schema: FormanSchemaField[];
243
+ /** Extra values injected into restore states, keyed by string path (dot notation, `[index]`, backtick-escaping). */
244
+ restoreExtras?: Record<string, Record<string, unknown>>;
242
245
  }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
243
246
  /**
244
247
  * Validates a simple Forman values against a schema
245
248
  * @param values The values to validate
246
249
  * @param schema The schema to validate against
247
250
  * @param options The validation options
251
+ * @param restoreExtras Values to be injected into restore objects of particular fields.
252
+ * Keyed by string path using dot notation for nested keys, `[index]` for array indices,
253
+ * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
248
254
  * @returns The validation result
249
255
  */
250
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
256
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
251
257
 
252
258
  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
@@ -69,6 +69,60 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
69
69
  const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
70
70
  return found;
71
71
  }
72
+ function pathToString(path) {
73
+ let result = "";
74
+ for (const key of path) {
75
+ if (typeof key === "number") {
76
+ result += `[${key}]`;
77
+ } else {
78
+ if (result.length > 0) {
79
+ result += ".";
80
+ }
81
+ if (key.includes("`")) {
82
+ throw new Error(`Invalid path key: backticks are not allowed in key "${key}"`);
83
+ }
84
+ result += key.includes(".") ? `\`${key}\`` : key;
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ function stringToPath(str) {
90
+ const path = [];
91
+ let i = 0;
92
+ while (i < str.length) {
93
+ if (str[i] === ".") {
94
+ i++;
95
+ if (i >= str.length) break;
96
+ }
97
+ if (str[i] === "[") {
98
+ const end = str.indexOf("]", i);
99
+ if (end === -1) {
100
+ throw new Error(`Invalid path: missing closing bracket in "${str}"`);
101
+ }
102
+ const num = Number(str.slice(i + 1, end));
103
+ if (!Number.isInteger(num) || num < 0) {
104
+ throw new Error(`Invalid path: non-numeric or negative index in "${str}"`);
105
+ }
106
+ path.push(num);
107
+ i = end + 1;
108
+ } else if (str[i] === "`") {
109
+ const end = str.indexOf("`", i + 1);
110
+ if (end === -1) {
111
+ throw new Error(`Invalid path: missing closing backtick in "${str}"`);
112
+ }
113
+ path.push(str.slice(i + 1, end));
114
+ i = end + 1;
115
+ } else {
116
+ let end = i;
117
+ while (end < str.length && str[end] !== "." && str[end] !== "[") {
118
+ end++;
119
+ }
120
+ path.push(str.slice(i, end));
121
+ i = end;
122
+ }
123
+ }
124
+ return path;
125
+ }
72
126
  function buildRestoreStructure(items) {
73
127
  const result = {};
74
128
  for (const item of items) {
@@ -502,7 +556,8 @@ function validateFormanField(field) {
502
556
  }
503
557
  function appendQueryString(path, domain, tail) {
504
558
  if (path.startsWith("api://")) return path;
505
- const queryString = tail.map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
559
+ const existingParams = new Set(new URL(path).searchParams.keys());
560
+ const queryString = tail.filter((part) => !existingParams.has(part)).map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
506
561
  if (!queryString) return path;
507
562
  const separator = path.includes("?") ? "&" : "?";
508
563
  return `${path}${separator}${queryString}`;
@@ -1050,6 +1105,25 @@ async function validateFormanWithDomainsInternal(domains, options) {
1050
1105
  }
1051
1106
  );
1052
1107
  errors.push(...result.errors);
1108
+ const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1109
+ if (restoreExtras) {
1110
+ const fieldStateMap = new Map(
1111
+ roots[domain].fieldStates.map(({ path, state }) => {
1112
+ return [pathToString(path), state];
1113
+ })
1114
+ );
1115
+ for (const [fieldPath, extra] of Object.entries(restoreExtras)) {
1116
+ const state = fieldStateMap.get(fieldPath);
1117
+ if (state) {
1118
+ state.extra = extra;
1119
+ } else {
1120
+ roots[domain].fieldStates.push({
1121
+ path: stringToPath(fieldPath),
1122
+ state: { extra }
1123
+ });
1124
+ }
1125
+ }
1126
+ }
1053
1127
  }
1054
1128
  return {
1055
1129
  valid: errors.length === 0,
@@ -1477,11 +1551,14 @@ async function handleSelectType(value, field, context) {
1477
1551
  if (resolved == null || typeof resolved !== "object") {
1478
1552
  return {
1479
1553
  valid: false,
1480
- errors: [...errors, {
1481
- domain: context.domain,
1482
- path: context.path.join("."),
1483
- message: `Remote resource ${optionsOrGroups} returned no data.`
1484
- }]
1554
+ errors: [
1555
+ ...errors,
1556
+ {
1557
+ domain: context.domain,
1558
+ path: context.path.join("."),
1559
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1560
+ }
1561
+ ]
1485
1562
  };
1486
1563
  }
1487
1564
  optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
@@ -1847,8 +1924,8 @@ function toJSONSchema(field) {
1847
1924
  function validateFormanWithDomains(domains, options) {
1848
1925
  return validateFormanWithDomainsInternal(domains, options);
1849
1926
  }
1850
- function validateForman(values, schema, options) {
1851
- return validateFormanWithDomains({ default: { values, schema } }, options);
1927
+ function validateForman(values, schema, options, restoreExtras) {
1928
+ return validateFormanWithDomains({ default: { values, schema, restoreExtras } }, options);
1852
1929
  }
1853
1930
  export {
1854
1931
  toFormanSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.8.4",
3
+ "version": "1.9.1",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",