@makehq/forman-schema 1.9.6 → 1.11.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
@@ -1066,6 +1066,47 @@ function extractNestedFromField(field) {
1066
1066
  if (isObject(field.options) && field.options.nested) return field.options.nested;
1067
1067
  return void 0;
1068
1068
  }
1069
+ var SCHEMA_STRIP_KEYS = [
1070
+ "nested",
1071
+ "options",
1072
+ "help",
1073
+ "rpc",
1074
+ "sort",
1075
+ "disabled",
1076
+ "multiline",
1077
+ "tags",
1078
+ "extension",
1079
+ "codepage",
1080
+ "logic"
1081
+ ];
1082
+ function clampFieldForSchema(field) {
1083
+ const clamped = { ...field };
1084
+ for (const key of SCHEMA_STRIP_KEYS) {
1085
+ delete clamped[key];
1086
+ }
1087
+ if (field.spec != null) {
1088
+ clamped.spec = Array.isArray(field.spec) ? field.spec.map(clampFieldForSchema) : clampFieldForSchema(field.spec);
1089
+ }
1090
+ if (field.type === "select" && !field.validate?.enum) {
1091
+ const rawOptions = isObject(field.options) ? field.options.store : field.options;
1092
+ if (Array.isArray(rawOptions)) {
1093
+ const values = [];
1094
+ for (const item of rawOptions) {
1095
+ if ("options" in item) {
1096
+ for (const opt of item.options) {
1097
+ values.push(opt.value);
1098
+ }
1099
+ } else if ("value" in item) {
1100
+ values.push(item.value);
1101
+ }
1102
+ }
1103
+ if (values.length > 0) {
1104
+ clamped.validate = { ...clamped.validate, enum: values.map(String) };
1105
+ }
1106
+ }
1107
+ }
1108
+ return clamped;
1109
+ }
1069
1110
  var FORMAN_TYPE_MAP2 = {
1070
1111
  account: "number",
1071
1112
  hook: "number",
@@ -1110,11 +1151,13 @@ var FORMAN_TYPE_MAP2 = {
1110
1151
  };
1111
1152
  async function validateFormanWithDomainsInternal(domains, options) {
1112
1153
  const errors = [];
1154
+ const warnings = [];
1113
1155
  const roots = Object.keys(domains).reduce(
1114
1156
  (acc, domain) => {
1115
1157
  acc[domain] = {
1116
1158
  seenFields: /* @__PURE__ */ new Set(),
1117
1159
  fieldStates: [],
1160
+ schemaFields: [],
1118
1161
  validateFields: (fields, context) => {
1119
1162
  return validateFormanValue(
1120
1163
  domains[domain].values,
@@ -1173,6 +1216,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1173
1216
  }
1174
1217
  );
1175
1218
  errors.push(...result.errors);
1219
+ warnings.push(...result.warnings);
1176
1220
  const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1177
1221
  if (restoreExtras) {
1178
1222
  const fieldStateMap = new Map(
@@ -1208,16 +1252,19 @@ async function validateFormanWithDomainsInternal(domains, options) {
1208
1252
  return {
1209
1253
  valid: errors.length === 0,
1210
1254
  errors,
1255
+ warnings,
1211
1256
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1212
1257
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1213
- ) : void 0
1258
+ ) : void 0,
1259
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1214
1260
  };
1215
1261
  }
1216
1262
  async function validateFormanValue(value, field, context) {
1217
1263
  if (isVisualType(field.type)) {
1218
1264
  return {
1219
1265
  valid: true,
1220
- errors: []
1266
+ errors: [],
1267
+ warnings: []
1221
1268
  };
1222
1269
  }
1223
1270
  const normalizedField = normalizeFormanFieldType(field);
@@ -1230,7 +1277,8 @@ async function validateFormanValue(value, field, context) {
1230
1277
  path: context.path.join("."),
1231
1278
  message: "Field is mandatory."
1232
1279
  }
1233
- ]
1280
+ ],
1281
+ warnings: []
1234
1282
  };
1235
1283
  }
1236
1284
  if (value == null || value === "") {
@@ -1239,7 +1287,8 @@ async function validateFormanValue(value, field, context) {
1239
1287
  }
1240
1288
  return {
1241
1289
  valid: true,
1242
- errors: []
1290
+ errors: [],
1291
+ warnings: []
1243
1292
  };
1244
1293
  }
1245
1294
  const expectedType = FORMAN_TYPE_MAP2[normalizedField.type];
@@ -1254,7 +1303,8 @@ async function validateFormanValue(value, field, context) {
1254
1303
  path: context.path.join("."),
1255
1304
  message: `Expected type '${expectedType}', got type '${actualType}'.`
1256
1305
  }
1257
- ]
1306
+ ],
1307
+ warnings: []
1258
1308
  };
1259
1309
  }
1260
1310
  if (containsIMLExpression(value)) {
@@ -1267,7 +1317,8 @@ async function validateFormanValue(value, field, context) {
1267
1317
  path: context.path.join("."),
1268
1318
  message: "Value contains prohibited IML expression."
1269
1319
  }
1270
- ]
1320
+ ],
1321
+ warnings: []
1271
1322
  };
1272
1323
  }
1273
1324
  if (isEditableType(normalizedField.type)) {
@@ -1282,7 +1333,8 @@ async function validateFormanValue(value, field, context) {
1282
1333
  }
1283
1334
  return {
1284
1335
  valid: true,
1285
- errors: []
1336
+ errors: [],
1337
+ warnings: []
1286
1338
  };
1287
1339
  }
1288
1340
  if (normalizedField.type === "udttype") {
@@ -1317,6 +1369,7 @@ async function validateFormanValue(value, field, context) {
1317
1369
  }
1318
1370
  async function handleCollectionType2(value, field, context) {
1319
1371
  const errors = [];
1372
+ const warnings = [];
1320
1373
  const seen = context.path.length === 0 ? context.roots[context.domain].seenFields : /* @__PURE__ */ new Set();
1321
1374
  const path = context.path;
1322
1375
  if (Array.isArray(field.spec)) {
@@ -1343,7 +1396,8 @@ async function handleCollectionType2(value, field, context) {
1343
1396
  path: context.path.join("."),
1344
1397
  message: `Failed to resolve remote resource ${subField}: ${error}`
1345
1398
  }
1346
- ]
1399
+ ],
1400
+ warnings
1347
1401
  };
1348
1402
  }
1349
1403
  }
@@ -1359,6 +1413,9 @@ async function handleCollectionType2(value, field, context) {
1359
1413
  continue;
1360
1414
  }
1361
1415
  if (context.strict && !seen.has(subField.name)) seen.add(subField.name);
1416
+ if (path.length === 0) {
1417
+ context.roots[context.domain].schemaFields.push(clampFieldForSchema(subField));
1418
+ }
1362
1419
  const result = await validateFormanValue(value[subField.name], subField, {
1363
1420
  ...context,
1364
1421
  path: [...path, subField.name],
@@ -1376,15 +1433,20 @@ async function handleCollectionType2(value, field, context) {
1376
1433
  continue;
1377
1434
  }
1378
1435
  if (context2.strict && !seen.has(subField2.name)) seen.add(subField2.name);
1436
+ if (path.length === 0) {
1437
+ context2.roots[context2.domain].schemaFields.push(clampFieldForSchema(subField2));
1438
+ }
1379
1439
  const result2 = await validateFormanValue(value[subField2.name], subField2, {
1380
1440
  ...context2,
1381
1441
  path: [...path, subField2.name]
1382
1442
  });
1383
1443
  errors.push(...result2.errors);
1444
+ warnings.push(...result2.warnings);
1384
1445
  }
1385
1446
  }
1386
1447
  });
1387
1448
  errors.push(...result.errors);
1449
+ warnings.push(...result.warnings);
1388
1450
  }
1389
1451
  }
1390
1452
  if (context.strict && context.path.length > 0) {
@@ -1401,11 +1463,13 @@ async function handleCollectionType2(value, field, context) {
1401
1463
  }
1402
1464
  return {
1403
1465
  valid: errors.length === 0,
1404
- errors
1466
+ errors,
1467
+ warnings
1405
1468
  };
1406
1469
  }
1407
1470
  async function handleArrayType2(value, field, context) {
1408
1471
  const errors = [];
1472
+ const warnings = [];
1409
1473
  if (field.spec) {
1410
1474
  for (const [index, item] of value.entries()) {
1411
1475
  const result = await validateFormanValue(
@@ -1414,6 +1478,7 @@ async function handleArrayType2(value, field, context) {
1414
1478
  { ...context, path: [...context.path, index] }
1415
1479
  );
1416
1480
  errors.push(...result.errors);
1481
+ warnings.push(...result.warnings);
1417
1482
  }
1418
1483
  }
1419
1484
  if (field.validate) {
@@ -1434,7 +1499,8 @@ async function handleArrayType2(value, field, context) {
1434
1499
  }
1435
1500
  return {
1436
1501
  valid: errors.length === 0,
1437
- errors
1502
+ errors,
1503
+ warnings
1438
1504
  };
1439
1505
  }
1440
1506
  async function handleFilterType2(value, field, context) {
@@ -1491,6 +1557,7 @@ async function handleFilterType2(value, field, context) {
1491
1557
  }
1492
1558
  async function handlePathType(value, field, context) {
1493
1559
  const errors = [];
1560
+ const warnings = [];
1494
1561
  if (typeof value !== "string") {
1495
1562
  return {
1496
1563
  valid: false,
@@ -1501,7 +1568,8 @@ async function handlePathType(value, field, context) {
1501
1568
  path: context.path.join("."),
1502
1569
  message: `Expected type 'string' for path, got type '${typeof value}'.`
1503
1570
  }
1504
- ]
1571
+ ],
1572
+ warnings
1505
1573
  };
1506
1574
  }
1507
1575
  let showRoot = true;
@@ -1526,7 +1594,8 @@ async function handlePathType(value, field, context) {
1526
1594
  path: context.path.join("."),
1527
1595
  message: `Single level path cannot contain slashes.`
1528
1596
  }
1529
- ]
1597
+ ],
1598
+ warnings
1530
1599
  };
1531
1600
  }
1532
1601
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -1535,7 +1604,8 @@ async function handlePathType(value, field, context) {
1535
1604
  if (showRoot && value === "/" && field.type === "folder") {
1536
1605
  return {
1537
1606
  valid: true,
1538
- errors: []
1607
+ errors: [],
1608
+ warnings
1539
1609
  };
1540
1610
  }
1541
1611
  if (levels.length < 2) {
@@ -1548,11 +1618,13 @@ async function handlePathType(value, field, context) {
1548
1618
  path: context.path.join("."),
1549
1619
  message: `Invalid path "${value}" encountered.`
1550
1620
  }
1551
- ]
1621
+ ],
1622
+ warnings
1552
1623
  };
1553
1624
  }
1554
1625
  const selectedPath = [];
1555
1626
  let levelOptions = [];
1627
+ let optionsFromRPC = false;
1556
1628
  for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
1557
1629
  const isLastLevel = levelIndex === levels.length - 2;
1558
1630
  const levelSelectedValue = levels[levelIndex + 1];
@@ -1567,7 +1639,8 @@ async function handlePathType(value, field, context) {
1567
1639
  path: context.path.join("."),
1568
1640
  message: `Invalid selected value of "${value}" encountered.`
1569
1641
  }
1570
- ]
1642
+ ],
1643
+ warnings
1571
1644
  };
1572
1645
  }
1573
1646
  if (typeof options === "string") {
@@ -1575,6 +1648,7 @@ async function handlePathType(value, field, context) {
1575
1648
  levelOptions = await context.resolveRemote(options, context, {
1576
1649
  [field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
1577
1650
  });
1651
+ optionsFromRPC = true;
1578
1652
  } catch (error) {
1579
1653
  return {
1580
1654
  valid: false,
@@ -1585,7 +1659,8 @@ async function handlePathType(value, field, context) {
1585
1659
  path: context.path.join("."),
1586
1660
  message: `Failed to resolve remote resource ${options}: ${error}`
1587
1661
  }
1588
- ]
1662
+ ],
1663
+ warnings
1589
1664
  };
1590
1665
  }
1591
1666
  } else if (isLastLevel) {
@@ -1602,6 +1677,14 @@ async function handlePathType(value, field, context) {
1602
1677
  });
1603
1678
  const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
1604
1679
  if (!selectedOption) {
1680
+ if (optionsFromRPC) {
1681
+ warnings.push({
1682
+ domain: context.domain,
1683
+ path: context.path.join("."),
1684
+ message: `Path '${levelSelectedValue}' not found in options.`
1685
+ });
1686
+ break;
1687
+ }
1605
1688
  return {
1606
1689
  valid: false,
1607
1690
  errors: [
@@ -1611,7 +1694,8 @@ async function handlePathType(value, field, context) {
1611
1694
  path: context.path.join("."),
1612
1695
  message: `Path '${levelSelectedValue}' not found in options.`
1613
1696
  }
1614
- ]
1697
+ ],
1698
+ warnings
1615
1699
  };
1616
1700
  }
1617
1701
  selectedPath.push(selectedOption);
@@ -1628,16 +1712,20 @@ async function handlePathType(value, field, context) {
1628
1712
  if (nested) {
1629
1713
  const result = await handleNestedFields(nested, value, field, context);
1630
1714
  errors.push(...result.errors);
1715
+ warnings.push(...result.warnings);
1631
1716
  }
1632
1717
  return {
1633
1718
  valid: errors.length === 0,
1634
- errors
1719
+ errors,
1720
+ warnings
1635
1721
  };
1636
1722
  }
1637
1723
  async function handleSelectType(value, field, context) {
1638
1724
  const errors = [];
1725
+ const warnings = [];
1639
1726
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
1640
1727
  let nested = extractNestedFromField(field);
1728
+ let optionsFromRPC = false;
1641
1729
  if (typeof optionsOrGroups === "string") {
1642
1730
  try {
1643
1731
  const resolved = await context.resolveRemote(optionsOrGroups, context);
@@ -1651,10 +1739,12 @@ async function handleSelectType(value, field, context) {
1651
1739
  path: context.path.join("."),
1652
1740
  message: `Remote resource ${optionsOrGroups} returned no data.`
1653
1741
  }
1654
- ]
1742
+ ],
1743
+ warnings
1655
1744
  };
1656
1745
  }
1657
1746
  optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1747
+ optionsFromRPC = true;
1658
1748
  } catch (error) {
1659
1749
  return {
1660
1750
  valid: false,
@@ -1665,7 +1755,8 @@ async function handleSelectType(value, field, context) {
1665
1755
  path: context.path.join("."),
1666
1756
  message: `Failed to resolve remote resource ${optionsOrGroups}: ${error}`
1667
1757
  }
1668
- ]
1758
+ ],
1759
+ warnings
1669
1760
  };
1670
1761
  }
1671
1762
  }
@@ -1680,7 +1771,8 @@ async function handleSelectType(value, field, context) {
1680
1771
  path: context.path.join("."),
1681
1772
  message: `Value is not an array.`
1682
1773
  }
1683
- ]
1774
+ ],
1775
+ warnings
1684
1776
  };
1685
1777
  }
1686
1778
  for (const singleValue of value) {
@@ -1690,7 +1782,7 @@ async function handleSelectType(value, field, context) {
1690
1782
  optionsOrGroups
1691
1783
  );
1692
1784
  if (!found) {
1693
- errors.push({
1785
+ (optionsFromRPC ? warnings : errors).push({
1694
1786
  domain: context.domain,
1695
1787
  path: context.path.join("."),
1696
1788
  message: `Value '${singleValue}' not found in options.`
@@ -1728,40 +1820,53 @@ async function handleSelectType(value, field, context) {
1728
1820
  } else {
1729
1821
  const item = findValueInSelectOptions(field, value, optionsOrGroups);
1730
1822
  if (!item) {
1731
- return {
1732
- valid: false,
1733
- errors: [
1734
- ...errors,
1735
- {
1736
- domain: context.domain,
1737
- path: context.path.join("."),
1738
- message: `Value '${value}' not found in options.`
1823
+ if (optionsFromRPC) {
1824
+ warnings.push({
1825
+ domain: context.domain,
1826
+ path: context.path.join("."),
1827
+ message: `Value '${value}' not found in options.`
1828
+ });
1829
+ } else {
1830
+ return {
1831
+ valid: false,
1832
+ errors: [
1833
+ ...errors,
1834
+ {
1835
+ domain: context.domain,
1836
+ path: context.path.join("."),
1837
+ message: `Value '${value}' not found in options.`
1838
+ }
1839
+ ],
1840
+ warnings
1841
+ };
1842
+ }
1843
+ } else {
1844
+ if (item.label) {
1845
+ context.roots[context.domain].fieldStates.push({
1846
+ path: context.path,
1847
+ state: {
1848
+ mode: isReferenceType(field.type) ? void 0 : "chose",
1849
+ label: item.label
1739
1850
  }
1740
- ]
1741
- };
1742
- }
1743
- if (item.label) {
1744
- context.roots[context.domain].fieldStates.push({
1745
- path: context.path,
1746
- state: {
1747
- mode: isReferenceType(field.type) ? void 0 : "chose",
1748
- label: item.label
1749
- }
1750
- });
1851
+ });
1852
+ }
1853
+ if (item.nested) nested = item.nested;
1751
1854
  }
1752
- if (item.nested) nested = item.nested;
1753
1855
  }
1754
1856
  if (nested) {
1755
1857
  const result = await handleNestedFields(nested, value, field, context);
1756
1858
  errors.push(...result.errors);
1859
+ warnings.push(...result.warnings);
1757
1860
  }
1758
1861
  return {
1759
1862
  valid: errors.length === 0,
1760
- errors
1863
+ errors,
1864
+ warnings
1761
1865
  };
1762
1866
  }
1763
1867
  async function handleNestedFields(nested, value, field, context) {
1764
1868
  const errors = [];
1869
+ const warnings = [];
1765
1870
  let store = isObject(nested) ? nested.store : nested;
1766
1871
  const domain = isObject(nested) && nested.domain ? nested.domain : void 0;
1767
1872
  context = {
@@ -1792,7 +1897,8 @@ async function handleNestedFields(nested, value, field, context) {
1792
1897
  path: context.path.join("."),
1793
1898
  message: `Failed to resolve remote resource ${item}: ${error}`
1794
1899
  }
1795
- ]
1900
+ ],
1901
+ warnings
1796
1902
  };
1797
1903
  }
1798
1904
  } else {
@@ -1812,17 +1918,20 @@ async function handleNestedFields(nested, value, field, context) {
1812
1918
  } else {
1813
1919
  const result = await context.roots[resolvedDomain].validateFields(store, context);
1814
1920
  errors.push(...result.errors);
1921
+ warnings.push(...result.warnings);
1815
1922
  }
1816
1923
  } else if (store) {
1817
1924
  await context.validateNestedFields(store, context);
1818
1925
  }
1819
1926
  return {
1820
1927
  valid: errors.length === 0,
1821
- errors
1928
+ errors,
1929
+ warnings
1822
1930
  };
1823
1931
  }
1824
1932
  async function handlePrimitiveType2(value, field, context) {
1825
1933
  const errors = [];
1934
+ const warnings = [];
1826
1935
  if (field.validate) {
1827
1936
  if (typeof value === "string") {
1828
1937
  if (field.validate.pattern && !new RegExp(
@@ -1876,10 +1985,12 @@ async function handlePrimitiveType2(value, field, context) {
1876
1985
  if (nested) {
1877
1986
  const result = await handleNestedFields(nested, value, field, context);
1878
1987
  errors.push(...result.errors);
1988
+ warnings.push(...result.warnings);
1879
1989
  }
1880
1990
  return {
1881
1991
  valid: errors.length === 0,
1882
- errors
1992
+ errors,
1993
+ warnings
1883
1994
  };
1884
1995
  }
1885
1996
 
package/dist/index.d.cts CHANGED
@@ -197,8 +197,19 @@ type FormanValidationResult = {
197
197
  /** Error message */
198
198
  message: string;
199
199
  }[];
200
+ /** Warnings (do not affect validity) */
201
+ warnings: {
202
+ /** Field domain */
203
+ domain: string;
204
+ /** Field path */
205
+ path: string;
206
+ /** Warning message */
207
+ message: string;
208
+ }[];
200
209
  /** States of fields grouped by domain */
201
210
  states?: Record<string, FormanSchemaFieldState>;
211
+ /** Resolved schema fields per domain */
212
+ schemas?: Record<string, FormanSchemaField[]>;
202
213
  };
203
214
  type FormanSchemaFieldState = {
204
215
  mode?: 'chose' | 'edit';
@@ -214,6 +225,8 @@ type FormanValidationOptions = {
214
225
  strict?: boolean;
215
226
  /** Whether to generate states for fields */
216
227
  states?: boolean;
228
+ /** Whether to collect resolved schema fields per domain */
229
+ schemas?: boolean;
217
230
  /** Remote resource resolver */
218
231
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
219
232
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
package/dist/index.d.ts CHANGED
@@ -197,8 +197,19 @@ type FormanValidationResult = {
197
197
  /** Error message */
198
198
  message: string;
199
199
  }[];
200
+ /** Warnings (do not affect validity) */
201
+ warnings: {
202
+ /** Field domain */
203
+ domain: string;
204
+ /** Field path */
205
+ path: string;
206
+ /** Warning message */
207
+ message: string;
208
+ }[];
200
209
  /** States of fields grouped by domain */
201
210
  states?: Record<string, FormanSchemaFieldState>;
211
+ /** Resolved schema fields per domain */
212
+ schemas?: Record<string, FormanSchemaField[]>;
202
213
  };
203
214
  type FormanSchemaFieldState = {
204
215
  mode?: 'chose' | 'edit';
@@ -214,6 +225,8 @@ type FormanValidationOptions = {
214
225
  strict?: boolean;
215
226
  /** Whether to generate states for fields */
216
227
  states?: boolean;
228
+ /** Whether to collect resolved schema fields per domain */
229
+ schemas?: boolean;
217
230
  /** Remote resource resolver */
218
231
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
219
232
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
package/dist/index.js CHANGED
@@ -1037,6 +1037,47 @@ function extractNestedFromField(field) {
1037
1037
  if (isObject(field.options) && field.options.nested) return field.options.nested;
1038
1038
  return void 0;
1039
1039
  }
1040
+ var SCHEMA_STRIP_KEYS = [
1041
+ "nested",
1042
+ "options",
1043
+ "help",
1044
+ "rpc",
1045
+ "sort",
1046
+ "disabled",
1047
+ "multiline",
1048
+ "tags",
1049
+ "extension",
1050
+ "codepage",
1051
+ "logic"
1052
+ ];
1053
+ function clampFieldForSchema(field) {
1054
+ const clamped = { ...field };
1055
+ for (const key of SCHEMA_STRIP_KEYS) {
1056
+ delete clamped[key];
1057
+ }
1058
+ if (field.spec != null) {
1059
+ clamped.spec = Array.isArray(field.spec) ? field.spec.map(clampFieldForSchema) : clampFieldForSchema(field.spec);
1060
+ }
1061
+ if (field.type === "select" && !field.validate?.enum) {
1062
+ const rawOptions = isObject(field.options) ? field.options.store : field.options;
1063
+ if (Array.isArray(rawOptions)) {
1064
+ const values = [];
1065
+ for (const item of rawOptions) {
1066
+ if ("options" in item) {
1067
+ for (const opt of item.options) {
1068
+ values.push(opt.value);
1069
+ }
1070
+ } else if ("value" in item) {
1071
+ values.push(item.value);
1072
+ }
1073
+ }
1074
+ if (values.length > 0) {
1075
+ clamped.validate = { ...clamped.validate, enum: values.map(String) };
1076
+ }
1077
+ }
1078
+ }
1079
+ return clamped;
1080
+ }
1040
1081
  var FORMAN_TYPE_MAP2 = {
1041
1082
  account: "number",
1042
1083
  hook: "number",
@@ -1081,11 +1122,13 @@ var FORMAN_TYPE_MAP2 = {
1081
1122
  };
1082
1123
  async function validateFormanWithDomainsInternal(domains, options) {
1083
1124
  const errors = [];
1125
+ const warnings = [];
1084
1126
  const roots = Object.keys(domains).reduce(
1085
1127
  (acc, domain) => {
1086
1128
  acc[domain] = {
1087
1129
  seenFields: /* @__PURE__ */ new Set(),
1088
1130
  fieldStates: [],
1131
+ schemaFields: [],
1089
1132
  validateFields: (fields, context) => {
1090
1133
  return validateFormanValue(
1091
1134
  domains[domain].values,
@@ -1144,6 +1187,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1144
1187
  }
1145
1188
  );
1146
1189
  errors.push(...result.errors);
1190
+ warnings.push(...result.warnings);
1147
1191
  const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1148
1192
  if (restoreExtras) {
1149
1193
  const fieldStateMap = new Map(
@@ -1179,16 +1223,19 @@ async function validateFormanWithDomainsInternal(domains, options) {
1179
1223
  return {
1180
1224
  valid: errors.length === 0,
1181
1225
  errors,
1226
+ warnings,
1182
1227
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1183
1228
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1184
- ) : void 0
1229
+ ) : void 0,
1230
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1185
1231
  };
1186
1232
  }
1187
1233
  async function validateFormanValue(value, field, context) {
1188
1234
  if (isVisualType(field.type)) {
1189
1235
  return {
1190
1236
  valid: true,
1191
- errors: []
1237
+ errors: [],
1238
+ warnings: []
1192
1239
  };
1193
1240
  }
1194
1241
  const normalizedField = normalizeFormanFieldType(field);
@@ -1201,7 +1248,8 @@ async function validateFormanValue(value, field, context) {
1201
1248
  path: context.path.join("."),
1202
1249
  message: "Field is mandatory."
1203
1250
  }
1204
- ]
1251
+ ],
1252
+ warnings: []
1205
1253
  };
1206
1254
  }
1207
1255
  if (value == null || value === "") {
@@ -1210,7 +1258,8 @@ async function validateFormanValue(value, field, context) {
1210
1258
  }
1211
1259
  return {
1212
1260
  valid: true,
1213
- errors: []
1261
+ errors: [],
1262
+ warnings: []
1214
1263
  };
1215
1264
  }
1216
1265
  const expectedType = FORMAN_TYPE_MAP2[normalizedField.type];
@@ -1225,7 +1274,8 @@ async function validateFormanValue(value, field, context) {
1225
1274
  path: context.path.join("."),
1226
1275
  message: `Expected type '${expectedType}', got type '${actualType}'.`
1227
1276
  }
1228
- ]
1277
+ ],
1278
+ warnings: []
1229
1279
  };
1230
1280
  }
1231
1281
  if (containsIMLExpression(value)) {
@@ -1238,7 +1288,8 @@ async function validateFormanValue(value, field, context) {
1238
1288
  path: context.path.join("."),
1239
1289
  message: "Value contains prohibited IML expression."
1240
1290
  }
1241
- ]
1291
+ ],
1292
+ warnings: []
1242
1293
  };
1243
1294
  }
1244
1295
  if (isEditableType(normalizedField.type)) {
@@ -1253,7 +1304,8 @@ async function validateFormanValue(value, field, context) {
1253
1304
  }
1254
1305
  return {
1255
1306
  valid: true,
1256
- errors: []
1307
+ errors: [],
1308
+ warnings: []
1257
1309
  };
1258
1310
  }
1259
1311
  if (normalizedField.type === "udttype") {
@@ -1288,6 +1340,7 @@ async function validateFormanValue(value, field, context) {
1288
1340
  }
1289
1341
  async function handleCollectionType2(value, field, context) {
1290
1342
  const errors = [];
1343
+ const warnings = [];
1291
1344
  const seen = context.path.length === 0 ? context.roots[context.domain].seenFields : /* @__PURE__ */ new Set();
1292
1345
  const path = context.path;
1293
1346
  if (Array.isArray(field.spec)) {
@@ -1314,7 +1367,8 @@ async function handleCollectionType2(value, field, context) {
1314
1367
  path: context.path.join("."),
1315
1368
  message: `Failed to resolve remote resource ${subField}: ${error}`
1316
1369
  }
1317
- ]
1370
+ ],
1371
+ warnings
1318
1372
  };
1319
1373
  }
1320
1374
  }
@@ -1330,6 +1384,9 @@ async function handleCollectionType2(value, field, context) {
1330
1384
  continue;
1331
1385
  }
1332
1386
  if (context.strict && !seen.has(subField.name)) seen.add(subField.name);
1387
+ if (path.length === 0) {
1388
+ context.roots[context.domain].schemaFields.push(clampFieldForSchema(subField));
1389
+ }
1333
1390
  const result = await validateFormanValue(value[subField.name], subField, {
1334
1391
  ...context,
1335
1392
  path: [...path, subField.name],
@@ -1347,15 +1404,20 @@ async function handleCollectionType2(value, field, context) {
1347
1404
  continue;
1348
1405
  }
1349
1406
  if (context2.strict && !seen.has(subField2.name)) seen.add(subField2.name);
1407
+ if (path.length === 0) {
1408
+ context2.roots[context2.domain].schemaFields.push(clampFieldForSchema(subField2));
1409
+ }
1350
1410
  const result2 = await validateFormanValue(value[subField2.name], subField2, {
1351
1411
  ...context2,
1352
1412
  path: [...path, subField2.name]
1353
1413
  });
1354
1414
  errors.push(...result2.errors);
1415
+ warnings.push(...result2.warnings);
1355
1416
  }
1356
1417
  }
1357
1418
  });
1358
1419
  errors.push(...result.errors);
1420
+ warnings.push(...result.warnings);
1359
1421
  }
1360
1422
  }
1361
1423
  if (context.strict && context.path.length > 0) {
@@ -1372,11 +1434,13 @@ async function handleCollectionType2(value, field, context) {
1372
1434
  }
1373
1435
  return {
1374
1436
  valid: errors.length === 0,
1375
- errors
1437
+ errors,
1438
+ warnings
1376
1439
  };
1377
1440
  }
1378
1441
  async function handleArrayType2(value, field, context) {
1379
1442
  const errors = [];
1443
+ const warnings = [];
1380
1444
  if (field.spec) {
1381
1445
  for (const [index, item] of value.entries()) {
1382
1446
  const result = await validateFormanValue(
@@ -1385,6 +1449,7 @@ async function handleArrayType2(value, field, context) {
1385
1449
  { ...context, path: [...context.path, index] }
1386
1450
  );
1387
1451
  errors.push(...result.errors);
1452
+ warnings.push(...result.warnings);
1388
1453
  }
1389
1454
  }
1390
1455
  if (field.validate) {
@@ -1405,7 +1470,8 @@ async function handleArrayType2(value, field, context) {
1405
1470
  }
1406
1471
  return {
1407
1472
  valid: errors.length === 0,
1408
- errors
1473
+ errors,
1474
+ warnings
1409
1475
  };
1410
1476
  }
1411
1477
  async function handleFilterType2(value, field, context) {
@@ -1462,6 +1528,7 @@ async function handleFilterType2(value, field, context) {
1462
1528
  }
1463
1529
  async function handlePathType(value, field, context) {
1464
1530
  const errors = [];
1531
+ const warnings = [];
1465
1532
  if (typeof value !== "string") {
1466
1533
  return {
1467
1534
  valid: false,
@@ -1472,7 +1539,8 @@ async function handlePathType(value, field, context) {
1472
1539
  path: context.path.join("."),
1473
1540
  message: `Expected type 'string' for path, got type '${typeof value}'.`
1474
1541
  }
1475
- ]
1542
+ ],
1543
+ warnings
1476
1544
  };
1477
1545
  }
1478
1546
  let showRoot = true;
@@ -1497,7 +1565,8 @@ async function handlePathType(value, field, context) {
1497
1565
  path: context.path.join("."),
1498
1566
  message: `Single level path cannot contain slashes.`
1499
1567
  }
1500
- ]
1568
+ ],
1569
+ warnings
1501
1570
  };
1502
1571
  }
1503
1572
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
@@ -1506,7 +1575,8 @@ async function handlePathType(value, field, context) {
1506
1575
  if (showRoot && value === "/" && field.type === "folder") {
1507
1576
  return {
1508
1577
  valid: true,
1509
- errors: []
1578
+ errors: [],
1579
+ warnings
1510
1580
  };
1511
1581
  }
1512
1582
  if (levels.length < 2) {
@@ -1519,11 +1589,13 @@ async function handlePathType(value, field, context) {
1519
1589
  path: context.path.join("."),
1520
1590
  message: `Invalid path "${value}" encountered.`
1521
1591
  }
1522
- ]
1592
+ ],
1593
+ warnings
1523
1594
  };
1524
1595
  }
1525
1596
  const selectedPath = [];
1526
1597
  let levelOptions = [];
1598
+ let optionsFromRPC = false;
1527
1599
  for (let levelIndex = 0; levelIndex < levels.length - 1; ++levelIndex) {
1528
1600
  const isLastLevel = levelIndex === levels.length - 2;
1529
1601
  const levelSelectedValue = levels[levelIndex + 1];
@@ -1538,7 +1610,8 @@ async function handlePathType(value, field, context) {
1538
1610
  path: context.path.join("."),
1539
1611
  message: `Invalid selected value of "${value}" encountered.`
1540
1612
  }
1541
- ]
1613
+ ],
1614
+ warnings
1542
1615
  };
1543
1616
  }
1544
1617
  if (typeof options === "string") {
@@ -1546,6 +1619,7 @@ async function handlePathType(value, field, context) {
1546
1619
  levelOptions = await context.resolveRemote(options, context, {
1547
1620
  [field.name]: (showRoot && !selectedPathValue.startsWith("/") ? "/" : "") + selectedPathValue
1548
1621
  });
1622
+ optionsFromRPC = true;
1549
1623
  } catch (error) {
1550
1624
  return {
1551
1625
  valid: false,
@@ -1556,7 +1630,8 @@ async function handlePathType(value, field, context) {
1556
1630
  path: context.path.join("."),
1557
1631
  message: `Failed to resolve remote resource ${options}: ${error}`
1558
1632
  }
1559
- ]
1633
+ ],
1634
+ warnings
1560
1635
  };
1561
1636
  }
1562
1637
  } else if (isLastLevel) {
@@ -1573,6 +1648,14 @@ async function handlePathType(value, field, context) {
1573
1648
  });
1574
1649
  const selectedOption = selectableOptions.find((candidate) => candidate.value === levelSelectedValue);
1575
1650
  if (!selectedOption) {
1651
+ if (optionsFromRPC) {
1652
+ warnings.push({
1653
+ domain: context.domain,
1654
+ path: context.path.join("."),
1655
+ message: `Path '${levelSelectedValue}' not found in options.`
1656
+ });
1657
+ break;
1658
+ }
1576
1659
  return {
1577
1660
  valid: false,
1578
1661
  errors: [
@@ -1582,7 +1665,8 @@ async function handlePathType(value, field, context) {
1582
1665
  path: context.path.join("."),
1583
1666
  message: `Path '${levelSelectedValue}' not found in options.`
1584
1667
  }
1585
- ]
1668
+ ],
1669
+ warnings
1586
1670
  };
1587
1671
  }
1588
1672
  selectedPath.push(selectedOption);
@@ -1599,16 +1683,20 @@ async function handlePathType(value, field, context) {
1599
1683
  if (nested) {
1600
1684
  const result = await handleNestedFields(nested, value, field, context);
1601
1685
  errors.push(...result.errors);
1686
+ warnings.push(...result.warnings);
1602
1687
  }
1603
1688
  return {
1604
1689
  valid: errors.length === 0,
1605
- errors
1690
+ errors,
1691
+ warnings
1606
1692
  };
1607
1693
  }
1608
1694
  async function handleSelectType(value, field, context) {
1609
1695
  const errors = [];
1696
+ const warnings = [];
1610
1697
  let optionsOrGroups = isObject(field.options) ? field.options.store : field.options;
1611
1698
  let nested = extractNestedFromField(field);
1699
+ let optionsFromRPC = false;
1612
1700
  if (typeof optionsOrGroups === "string") {
1613
1701
  try {
1614
1702
  const resolved = await context.resolveRemote(optionsOrGroups, context);
@@ -1622,10 +1710,12 @@ async function handleSelectType(value, field, context) {
1622
1710
  path: context.path.join("."),
1623
1711
  message: `Remote resource ${optionsOrGroups} returned no data.`
1624
1712
  }
1625
- ]
1713
+ ],
1714
+ warnings
1626
1715
  };
1627
1716
  }
1628
1717
  optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1718
+ optionsFromRPC = true;
1629
1719
  } catch (error) {
1630
1720
  return {
1631
1721
  valid: false,
@@ -1636,7 +1726,8 @@ async function handleSelectType(value, field, context) {
1636
1726
  path: context.path.join("."),
1637
1727
  message: `Failed to resolve remote resource ${optionsOrGroups}: ${error}`
1638
1728
  }
1639
- ]
1729
+ ],
1730
+ warnings
1640
1731
  };
1641
1732
  }
1642
1733
  }
@@ -1651,7 +1742,8 @@ async function handleSelectType(value, field, context) {
1651
1742
  path: context.path.join("."),
1652
1743
  message: `Value is not an array.`
1653
1744
  }
1654
- ]
1745
+ ],
1746
+ warnings
1655
1747
  };
1656
1748
  }
1657
1749
  for (const singleValue of value) {
@@ -1661,7 +1753,7 @@ async function handleSelectType(value, field, context) {
1661
1753
  optionsOrGroups
1662
1754
  );
1663
1755
  if (!found) {
1664
- errors.push({
1756
+ (optionsFromRPC ? warnings : errors).push({
1665
1757
  domain: context.domain,
1666
1758
  path: context.path.join("."),
1667
1759
  message: `Value '${singleValue}' not found in options.`
@@ -1699,40 +1791,53 @@ async function handleSelectType(value, field, context) {
1699
1791
  } else {
1700
1792
  const item = findValueInSelectOptions(field, value, optionsOrGroups);
1701
1793
  if (!item) {
1702
- return {
1703
- valid: false,
1704
- errors: [
1705
- ...errors,
1706
- {
1707
- domain: context.domain,
1708
- path: context.path.join("."),
1709
- message: `Value '${value}' not found in options.`
1794
+ if (optionsFromRPC) {
1795
+ warnings.push({
1796
+ domain: context.domain,
1797
+ path: context.path.join("."),
1798
+ message: `Value '${value}' not found in options.`
1799
+ });
1800
+ } else {
1801
+ return {
1802
+ valid: false,
1803
+ errors: [
1804
+ ...errors,
1805
+ {
1806
+ domain: context.domain,
1807
+ path: context.path.join("."),
1808
+ message: `Value '${value}' not found in options.`
1809
+ }
1810
+ ],
1811
+ warnings
1812
+ };
1813
+ }
1814
+ } else {
1815
+ if (item.label) {
1816
+ context.roots[context.domain].fieldStates.push({
1817
+ path: context.path,
1818
+ state: {
1819
+ mode: isReferenceType(field.type) ? void 0 : "chose",
1820
+ label: item.label
1710
1821
  }
1711
- ]
1712
- };
1713
- }
1714
- if (item.label) {
1715
- context.roots[context.domain].fieldStates.push({
1716
- path: context.path,
1717
- state: {
1718
- mode: isReferenceType(field.type) ? void 0 : "chose",
1719
- label: item.label
1720
- }
1721
- });
1822
+ });
1823
+ }
1824
+ if (item.nested) nested = item.nested;
1722
1825
  }
1723
- if (item.nested) nested = item.nested;
1724
1826
  }
1725
1827
  if (nested) {
1726
1828
  const result = await handleNestedFields(nested, value, field, context);
1727
1829
  errors.push(...result.errors);
1830
+ warnings.push(...result.warnings);
1728
1831
  }
1729
1832
  return {
1730
1833
  valid: errors.length === 0,
1731
- errors
1834
+ errors,
1835
+ warnings
1732
1836
  };
1733
1837
  }
1734
1838
  async function handleNestedFields(nested, value, field, context) {
1735
1839
  const errors = [];
1840
+ const warnings = [];
1736
1841
  let store = isObject(nested) ? nested.store : nested;
1737
1842
  const domain = isObject(nested) && nested.domain ? nested.domain : void 0;
1738
1843
  context = {
@@ -1763,7 +1868,8 @@ async function handleNestedFields(nested, value, field, context) {
1763
1868
  path: context.path.join("."),
1764
1869
  message: `Failed to resolve remote resource ${item}: ${error}`
1765
1870
  }
1766
- ]
1871
+ ],
1872
+ warnings
1767
1873
  };
1768
1874
  }
1769
1875
  } else {
@@ -1783,17 +1889,20 @@ async function handleNestedFields(nested, value, field, context) {
1783
1889
  } else {
1784
1890
  const result = await context.roots[resolvedDomain].validateFields(store, context);
1785
1891
  errors.push(...result.errors);
1892
+ warnings.push(...result.warnings);
1786
1893
  }
1787
1894
  } else if (store) {
1788
1895
  await context.validateNestedFields(store, context);
1789
1896
  }
1790
1897
  return {
1791
1898
  valid: errors.length === 0,
1792
- errors
1899
+ errors,
1900
+ warnings
1793
1901
  };
1794
1902
  }
1795
1903
  async function handlePrimitiveType2(value, field, context) {
1796
1904
  const errors = [];
1905
+ const warnings = [];
1797
1906
  if (field.validate) {
1798
1907
  if (typeof value === "string") {
1799
1908
  if (field.validate.pattern && !new RegExp(
@@ -1847,10 +1956,12 @@ async function handlePrimitiveType2(value, field, context) {
1847
1956
  if (nested) {
1848
1957
  const result = await handleNestedFields(nested, value, field, context);
1849
1958
  errors.push(...result.errors);
1959
+ warnings.push(...result.warnings);
1850
1960
  }
1851
1961
  return {
1852
1962
  valid: errors.length === 0,
1853
- errors
1963
+ errors,
1964
+ warnings
1854
1965
  };
1855
1966
  }
1856
1967
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.9.6",
3
+ "version": "1.11.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",