@makehq/forman-schema 1.9.4 → 1.9.6

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
@@ -524,6 +524,7 @@ function udtspecCollapse(field) {
524
524
  }
525
525
 
526
526
  // src/forman.ts
527
+ var EMPTY_OPTION_DESCRIPTION = "Optional field, can be left empty.";
527
528
  var SchemaConversionError = class extends Error {
528
529
  /** Field that caused the error */
529
530
  field;
@@ -886,11 +887,10 @@ function handleSelectOrPathType(field, result, context) {
886
887
  const placeholder = field.options.placeholder;
887
888
  if (isObject(placeholder) && placeholder.nested) {
888
889
  (options ||= []).push({
889
- value: null,
890
+ value: "",
890
891
  label: placeholder.label,
891
892
  nested: placeholder.nested
892
893
  });
893
- result.type = [result.type, "null"];
894
894
  }
895
895
  }
896
896
  if (options?.some((option) => {
@@ -944,6 +944,19 @@ function handleSelectOrPathType(field, result, context) {
944
944
  result.enum = (options || []).map((option) => option.value);
945
945
  }
946
946
  }
947
+ if (!field.required) {
948
+ if (result.enum && !result.enum.includes("")) {
949
+ result.enum = ["", ...result.enum];
950
+ } else if (result.oneOf && !result.oneOf.some((entry) => isObject(entry) && entry.const === "")) {
951
+ result.oneOf = [{ title: "Empty", const: "" }, ...result.oneOf];
952
+ }
953
+ if (result.default === void 0) {
954
+ result.default = "";
955
+ }
956
+ if (!result.description) {
957
+ result.description = EMPTY_OPTION_DESCRIPTION;
958
+ }
959
+ }
947
960
  result = handleNestedWithDomain(field, nested, domain, result, context);
948
961
  if (field.rpc) result = processRpcDirective(field, result, context);
949
962
  return result;
@@ -1137,6 +1150,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1137
1150
  path: [],
1138
1151
  tail: [],
1139
1152
  strict: options?.strict === true,
1153
+ domainAliases: options?.domainAliases ?? {},
1140
1154
  validateNestedFields: () => {
1141
1155
  throw new Error("Cannot validate nested fields without parent field.");
1142
1156
  },
@@ -1179,6 +1193,18 @@ async function validateFormanWithDomainsInternal(domains, options) {
1179
1193
  }
1180
1194
  }
1181
1195
  }
1196
+ if (options?.strict) {
1197
+ for (const domain of Object.keys(domains)) {
1198
+ if (!domains[domain]) continue;
1199
+ const root = roots[domain];
1200
+ for (const key of Object.keys(domains[domain].values)) {
1201
+ if (!root.seenFields.has(key)) {
1202
+ root.seenFields.add(key);
1203
+ errors.push({ domain, path: "", message: `Unknown field '${key}'.` });
1204
+ }
1205
+ }
1206
+ }
1207
+ }
1182
1208
  return {
1183
1209
  valid: errors.length === 0,
1184
1210
  errors,
@@ -1207,7 +1233,7 @@ async function validateFormanValue(value, field, context) {
1207
1233
  ]
1208
1234
  };
1209
1235
  }
1210
- if (value == null) {
1236
+ if (value == null || value === "") {
1211
1237
  if (normalizedField.type === "select" && hasPlaceholderNested(normalizedField)) {
1212
1238
  return handleSelectType(value, normalizedField, context);
1213
1239
  }
@@ -1361,7 +1387,7 @@ async function handleCollectionType2(value, field, context) {
1361
1387
  errors.push(...result.errors);
1362
1388
  }
1363
1389
  }
1364
- if (context.strict) {
1390
+ if (context.strict && context.path.length > 0) {
1365
1391
  for (const key of Object.keys(value)) {
1366
1392
  if (!seen.has(key)) {
1367
1393
  seen.add(key);
@@ -1687,7 +1713,7 @@ async function handleSelectType(value, field, context) {
1687
1713
  });
1688
1714
  }
1689
1715
  }
1690
- } else if (value == null && hasPlaceholderNested(field)) {
1716
+ } else if ((value == null || value === "") && hasPlaceholderNested(field)) {
1691
1717
  const placeholder = field.options.placeholder;
1692
1718
  if (placeholder.label) {
1693
1719
  context.roots[context.domain].fieldStates.push({
@@ -1775,15 +1801,16 @@ async function handleNestedFields(nested, value, field, context) {
1775
1801
  }
1776
1802
  store = resolvedStore;
1777
1803
  }
1778
- if (store && domain && domain !== context.domain) {
1779
- if (!context.roots[domain]) {
1804
+ const resolvedDomain = domain ? context.domainAliases[domain] ?? domain : domain;
1805
+ if (store && resolvedDomain && resolvedDomain !== context.domain) {
1806
+ if (!context.roots[resolvedDomain]) {
1780
1807
  errors.push({
1781
1808
  domain: context.domain,
1782
1809
  path: context.path.join("."),
1783
- message: `Unable to process nested fields: Domain '${domain}' not found.`
1810
+ message: resolvedDomain !== domain ? `Unable to process nested fields: Resolved domain '${resolvedDomain}' (from alias '${domain}') not found.` : `Unable to process nested fields: Domain '${domain}' not found.`
1784
1811
  });
1785
1812
  } else {
1786
- const result = await context.roots[domain].validateFields(store, context);
1813
+ const result = await context.roots[resolvedDomain].validateFields(store, context);
1787
1814
  errors.push(...result.errors);
1788
1815
  }
1789
1816
  } else if (store) {
@@ -1919,10 +1946,11 @@ function toFormanSchema(field) {
1919
1946
  case "string":
1920
1947
  const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
1921
1948
  if (pathInfo) {
1949
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1922
1950
  return {
1923
1951
  type: pathInfo.value.type,
1924
1952
  label: noEmpty(field.title),
1925
- help: noEmpty(field.description),
1953
+ help,
1926
1954
  options: {
1927
1955
  store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
1928
1956
  showRoot: pathInfo.value.showRoot,
@@ -1931,18 +1959,20 @@ function toFormanSchema(field) {
1931
1959
  };
1932
1960
  }
1933
1961
  if (field.enum) {
1962
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1934
1963
  return {
1935
1964
  type: "select",
1936
1965
  label: noEmpty(field.title),
1937
- help: noEmpty(field.description),
1938
- options: field.enum.map((value) => ({ value }))
1966
+ help,
1967
+ options: field.enum.filter((value) => value !== "").map((value) => ({ value }))
1939
1968
  };
1940
1969
  } else if (field.oneOf) {
1970
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1941
1971
  return {
1942
1972
  type: "select",
1943
1973
  label: noEmpty(field.title),
1944
- help: noEmpty(field.description),
1945
- options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1974
+ help,
1975
+ options: field.oneOf.filter((value) => value && value.const !== "").map((value) => ({ value: value.const }))
1946
1976
  };
1947
1977
  }
1948
1978
  let textField = {
package/dist/index.d.cts CHANGED
@@ -216,6 +216,8 @@ type FormanValidationOptions = {
216
216
  states?: boolean;
217
217
  /** Remote resource resolver */
218
218
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
219
+ /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
220
+ domainAliases?: Record<string, string>;
219
221
  };
220
222
 
221
223
  /**
package/dist/index.d.ts CHANGED
@@ -216,6 +216,8 @@ type FormanValidationOptions = {
216
216
  states?: boolean;
217
217
  /** Remote resource resolver */
218
218
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
219
+ /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
220
+ domainAliases?: Record<string, string>;
219
221
  };
220
222
 
221
223
  /**
package/dist/index.js CHANGED
@@ -495,6 +495,7 @@ function udtspecCollapse(field) {
495
495
  }
496
496
 
497
497
  // src/forman.ts
498
+ var EMPTY_OPTION_DESCRIPTION = "Optional field, can be left empty.";
498
499
  var SchemaConversionError = class extends Error {
499
500
  /** Field that caused the error */
500
501
  field;
@@ -857,11 +858,10 @@ function handleSelectOrPathType(field, result, context) {
857
858
  const placeholder = field.options.placeholder;
858
859
  if (isObject(placeholder) && placeholder.nested) {
859
860
  (options ||= []).push({
860
- value: null,
861
+ value: "",
861
862
  label: placeholder.label,
862
863
  nested: placeholder.nested
863
864
  });
864
- result.type = [result.type, "null"];
865
865
  }
866
866
  }
867
867
  if (options?.some((option) => {
@@ -915,6 +915,19 @@ function handleSelectOrPathType(field, result, context) {
915
915
  result.enum = (options || []).map((option) => option.value);
916
916
  }
917
917
  }
918
+ if (!field.required) {
919
+ if (result.enum && !result.enum.includes("")) {
920
+ result.enum = ["", ...result.enum];
921
+ } else if (result.oneOf && !result.oneOf.some((entry) => isObject(entry) && entry.const === "")) {
922
+ result.oneOf = [{ title: "Empty", const: "" }, ...result.oneOf];
923
+ }
924
+ if (result.default === void 0) {
925
+ result.default = "";
926
+ }
927
+ if (!result.description) {
928
+ result.description = EMPTY_OPTION_DESCRIPTION;
929
+ }
930
+ }
918
931
  result = handleNestedWithDomain(field, nested, domain, result, context);
919
932
  if (field.rpc) result = processRpcDirective(field, result, context);
920
933
  return result;
@@ -1108,6 +1121,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1108
1121
  path: [],
1109
1122
  tail: [],
1110
1123
  strict: options?.strict === true,
1124
+ domainAliases: options?.domainAliases ?? {},
1111
1125
  validateNestedFields: () => {
1112
1126
  throw new Error("Cannot validate nested fields without parent field.");
1113
1127
  },
@@ -1150,6 +1164,18 @@ async function validateFormanWithDomainsInternal(domains, options) {
1150
1164
  }
1151
1165
  }
1152
1166
  }
1167
+ if (options?.strict) {
1168
+ for (const domain of Object.keys(domains)) {
1169
+ if (!domains[domain]) continue;
1170
+ const root = roots[domain];
1171
+ for (const key of Object.keys(domains[domain].values)) {
1172
+ if (!root.seenFields.has(key)) {
1173
+ root.seenFields.add(key);
1174
+ errors.push({ domain, path: "", message: `Unknown field '${key}'.` });
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1153
1179
  return {
1154
1180
  valid: errors.length === 0,
1155
1181
  errors,
@@ -1178,7 +1204,7 @@ async function validateFormanValue(value, field, context) {
1178
1204
  ]
1179
1205
  };
1180
1206
  }
1181
- if (value == null) {
1207
+ if (value == null || value === "") {
1182
1208
  if (normalizedField.type === "select" && hasPlaceholderNested(normalizedField)) {
1183
1209
  return handleSelectType(value, normalizedField, context);
1184
1210
  }
@@ -1332,7 +1358,7 @@ async function handleCollectionType2(value, field, context) {
1332
1358
  errors.push(...result.errors);
1333
1359
  }
1334
1360
  }
1335
- if (context.strict) {
1361
+ if (context.strict && context.path.length > 0) {
1336
1362
  for (const key of Object.keys(value)) {
1337
1363
  if (!seen.has(key)) {
1338
1364
  seen.add(key);
@@ -1658,7 +1684,7 @@ async function handleSelectType(value, field, context) {
1658
1684
  });
1659
1685
  }
1660
1686
  }
1661
- } else if (value == null && hasPlaceholderNested(field)) {
1687
+ } else if ((value == null || value === "") && hasPlaceholderNested(field)) {
1662
1688
  const placeholder = field.options.placeholder;
1663
1689
  if (placeholder.label) {
1664
1690
  context.roots[context.domain].fieldStates.push({
@@ -1746,15 +1772,16 @@ async function handleNestedFields(nested, value, field, context) {
1746
1772
  }
1747
1773
  store = resolvedStore;
1748
1774
  }
1749
- if (store && domain && domain !== context.domain) {
1750
- if (!context.roots[domain]) {
1775
+ const resolvedDomain = domain ? context.domainAliases[domain] ?? domain : domain;
1776
+ if (store && resolvedDomain && resolvedDomain !== context.domain) {
1777
+ if (!context.roots[resolvedDomain]) {
1751
1778
  errors.push({
1752
1779
  domain: context.domain,
1753
1780
  path: context.path.join("."),
1754
- message: `Unable to process nested fields: Domain '${domain}' not found.`
1781
+ message: resolvedDomain !== domain ? `Unable to process nested fields: Resolved domain '${resolvedDomain}' (from alias '${domain}') not found.` : `Unable to process nested fields: Domain '${domain}' not found.`
1755
1782
  });
1756
1783
  } else {
1757
- const result = await context.roots[domain].validateFields(store, context);
1784
+ const result = await context.roots[resolvedDomain].validateFields(store, context);
1758
1785
  errors.push(...result.errors);
1759
1786
  }
1760
1787
  } else if (store) {
@@ -1890,10 +1917,11 @@ function toFormanSchema(field) {
1890
1917
  case "string":
1891
1918
  const pathInfo = Object.getOwnPropertyDescriptor(field, "x-path");
1892
1919
  if (pathInfo) {
1920
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1893
1921
  return {
1894
1922
  type: pathInfo.value.type,
1895
1923
  label: noEmpty(field.title),
1896
- help: noEmpty(field.description),
1924
+ help,
1897
1925
  options: {
1898
1926
  store: Object.getOwnPropertyDescriptor(field, "x-fetch")?.value,
1899
1927
  showRoot: pathInfo.value.showRoot,
@@ -1902,18 +1930,20 @@ function toFormanSchema(field) {
1902
1930
  };
1903
1931
  }
1904
1932
  if (field.enum) {
1933
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1905
1934
  return {
1906
1935
  type: "select",
1907
1936
  label: noEmpty(field.title),
1908
- help: noEmpty(field.description),
1909
- options: field.enum.map((value) => ({ value }))
1937
+ help,
1938
+ options: field.enum.filter((value) => value !== "").map((value) => ({ value }))
1910
1939
  };
1911
1940
  } else if (field.oneOf) {
1941
+ const help = field.description === EMPTY_OPTION_DESCRIPTION ? void 0 : noEmpty(field.description);
1912
1942
  return {
1913
1943
  type: "select",
1914
1944
  label: noEmpty(field.title),
1915
- help: noEmpty(field.description),
1916
- options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
1945
+ help,
1946
+ options: field.oneOf.filter((value) => value && value.const !== "").map((value) => ({ value: value.const }))
1917
1947
  };
1918
1948
  }
1919
1949
  let textField = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",