@makehq/forman-schema 1.17.0 → 1.19.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
@@ -58,6 +58,9 @@ function noEmpty(text) {
58
58
  function isObject(value) {
59
59
  return typeof value === "object" && value !== null && !Array.isArray(value);
60
60
  }
61
+ function isBooleanBranchNested(nested) {
62
+ return isObject(nested) && !("store" in nested) && ("true" in nested || "false" in nested);
63
+ }
61
64
  function isOptionGroup(value) {
62
65
  return "options" in value && Array.isArray(value.options);
63
66
  }
@@ -1124,7 +1127,7 @@ function processRpcDirective(field, result, context) {
1124
1127
  return result;
1125
1128
  }
1126
1129
  function extractNestedAndDomain(field) {
1127
- const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isObject(field.nested) ? field.nested.store : field.nested;
1130
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isBooleanBranchNested(field.nested) ? void 0 : isObject(field.nested) ? field.nested.store : field.nested;
1128
1131
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : isObject(field.nested) && field.nested.domain ? field.nested.domain : void 0;
1129
1132
  return { nested, domain };
1130
1133
  }
@@ -1389,10 +1392,22 @@ async function validateFormanWithDomainsInternal(domains, options) {
1389
1392
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1390
1393
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1391
1394
  ) : void 0,
1392
- schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1395
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1396
+ // Same spec as `schemas`, but also on the failure path. Remote-resolved fields are
1397
+ // built before the verdict, so a rejection can name the fields it rejected instead of
1398
+ // leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
1399
+ // callers read its presence as a success signal.
1400
+ resolvedSchemas: options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1393
1401
  };
1394
1402
  }
1395
1403
  async function validateFormanValue(value, field, context) {
1404
+ if (context.registerOnly) {
1405
+ return {
1406
+ valid: true,
1407
+ errors: [],
1408
+ warnings: []
1409
+ };
1410
+ }
1396
1411
  if (isVisualType(field.type)) {
1397
1412
  return {
1398
1413
  valid: true,
@@ -1414,7 +1429,7 @@ async function validateFormanValue(value, field, context) {
1414
1429
  };
1415
1430
  }
1416
1431
  const normalizedField = normalizeFormanFieldType(field);
1417
- if (normalizedField.required && (value == null || value === "")) {
1432
+ if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
1418
1433
  return {
1419
1434
  valid: false,
1420
1435
  errors: [
@@ -1584,7 +1599,7 @@ async function handleCollectionType2(value, field, context) {
1584
1599
  continue;
1585
1600
  }
1586
1601
  if (context2.strict && !seen.has(subField2.name)) seen.add(subField2.name);
1587
- if (path.length === 0) {
1602
+ if (path.length === 0 && !context2.registerOnly) {
1588
1603
  context2.roots[context2.domain].schemaFields.push(clampFieldForSchema(subField2));
1589
1604
  }
1590
1605
  const result2 = await validateFormanValue(value[subField2.name], subField2, {
@@ -2177,7 +2192,7 @@ async function handlePrimitiveType2(value, field, context) {
2177
2192
  }
2178
2193
  const nested = extractNestedFromField(field);
2179
2194
  if (nested) {
2180
- const result = await handleNestedFields(nested, value, field, context);
2195
+ const result = FORMAN_TYPE_MAP2[field.type] === "boolean" ? await handleBooleanNestedFields(nested, value, field, context) : await handleNestedFields(nested, value, field, context);
2181
2196
  errors.push(...result.errors);
2182
2197
  warnings.push(...result.warnings);
2183
2198
  }
@@ -2187,6 +2202,19 @@ async function handlePrimitiveType2(value, field, context) {
2187
2202
  warnings
2188
2203
  };
2189
2204
  }
2205
+ async function handleBooleanNestedFields(nested, value, field, context) {
2206
+ if (isBooleanBranchNested(nested)) {
2207
+ const active2 = value === false ? nested.false : nested.true;
2208
+ const inactive = value === false ? nested.true : nested.false;
2209
+ const result = active2 ? await handleNestedFields(active2, value, field, context) : { valid: true, errors: [], warnings: [] };
2210
+ if (context.strict && inactive) {
2211
+ await handleNestedFields(inactive, value, field, { ...context, registerOnly: true });
2212
+ }
2213
+ return result;
2214
+ }
2215
+ const active = field.reversedNested === true ? value === false : value === true;
2216
+ return handleNestedFields(nested, value, field, active ? context : { ...context, suppressRequired: true });
2217
+ }
2190
2218
 
2191
2219
  // src/json.ts
2192
2220
  var JSON_PRIMITIVE_TYPE_MAP = {
package/dist/index.d.cts CHANGED
@@ -47,8 +47,8 @@ type FormanSchemaField = {
47
47
  advanced?: boolean;
48
48
  /** Human readable label for the field */
49
49
  label?: string;
50
- /** Nested fields */
51
- nested?: FormanSchemaNested;
50
+ nested?: FormanSchemaNested | FormanSchemaBooleanNested;
51
+ reversedNested?: boolean;
52
52
  /** Validation rules */
53
53
  validate?: FormanSchemaValidation;
54
54
  /** Whether the field is disabled (`false` by default) */
@@ -186,6 +186,10 @@ type FormanSchemaExtendedNested = {
186
186
  /** Domain for the nested fields */
187
187
  domain?: string;
188
188
  };
189
+ type FormanSchemaBooleanNested = {
190
+ true?: (FormanSchemaField | string)[] | string;
191
+ false?: (FormanSchemaField | string)[] | string;
192
+ };
189
193
  /**
190
194
  * Validation result
191
195
  */
@@ -212,8 +216,15 @@ type FormanValidationResult = {
212
216
  }[];
213
217
  /** States of fields grouped by domain */
214
218
  states?: Record<string, FormanSchemaFieldState>;
215
- /** Resolved schema fields per domain */
219
+ /** Resolved schema fields per domain. Requires `options.schemas`; only present when validation succeeded. */
216
220
  schemas?: Record<string, FormanSchemaField[]>;
221
+ /**
222
+ * Resolved schema fields per domain. Requires `options.schemas`, and unlike {@link schemas} is
223
+ * present whether or not validation succeeded — including fields revealed by a remote-resolved
224
+ * (`rpc://`) sub-form, which cannot be known from the static schema alone. Lets a caller report
225
+ * which fields it rejected.
226
+ */
227
+ resolvedSchemas?: Record<string, FormanSchemaField[]>;
217
228
  };
218
229
  type FormanSchemaFieldState = {
219
230
  mode?: 'chose' | 'edit';
@@ -402,4 +413,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
402
413
  */
403
414
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
404
415
 
405
- export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
416
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaBooleanNested, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
package/dist/index.d.ts CHANGED
@@ -47,8 +47,8 @@ type FormanSchemaField = {
47
47
  advanced?: boolean;
48
48
  /** Human readable label for the field */
49
49
  label?: string;
50
- /** Nested fields */
51
- nested?: FormanSchemaNested;
50
+ nested?: FormanSchemaNested | FormanSchemaBooleanNested;
51
+ reversedNested?: boolean;
52
52
  /** Validation rules */
53
53
  validate?: FormanSchemaValidation;
54
54
  /** Whether the field is disabled (`false` by default) */
@@ -186,6 +186,10 @@ type FormanSchemaExtendedNested = {
186
186
  /** Domain for the nested fields */
187
187
  domain?: string;
188
188
  };
189
+ type FormanSchemaBooleanNested = {
190
+ true?: (FormanSchemaField | string)[] | string;
191
+ false?: (FormanSchemaField | string)[] | string;
192
+ };
189
193
  /**
190
194
  * Validation result
191
195
  */
@@ -212,8 +216,15 @@ type FormanValidationResult = {
212
216
  }[];
213
217
  /** States of fields grouped by domain */
214
218
  states?: Record<string, FormanSchemaFieldState>;
215
- /** Resolved schema fields per domain */
219
+ /** Resolved schema fields per domain. Requires `options.schemas`; only present when validation succeeded. */
216
220
  schemas?: Record<string, FormanSchemaField[]>;
221
+ /**
222
+ * Resolved schema fields per domain. Requires `options.schemas`, and unlike {@link schemas} is
223
+ * present whether or not validation succeeded — including fields revealed by a remote-resolved
224
+ * (`rpc://`) sub-form, which cannot be known from the static schema alone. Lets a caller report
225
+ * which fields it rejected.
226
+ */
227
+ resolvedSchemas?: Record<string, FormanSchemaField[]>;
217
228
  };
218
229
  type FormanSchemaFieldState = {
219
230
  mode?: 'chose' | 'edit';
@@ -402,4 +413,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
402
413
  */
403
414
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
404
415
 
405
- export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
416
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanSchemaBooleanNested, type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, type FormanValidationOptions, type FormanValidationResult, SchemaConversionError, resolveFormanFieldType, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
package/dist/index.js CHANGED
@@ -26,6 +26,9 @@ function noEmpty(text) {
26
26
  function isObject(value) {
27
27
  return typeof value === "object" && value !== null && !Array.isArray(value);
28
28
  }
29
+ function isBooleanBranchNested(nested) {
30
+ return isObject(nested) && !("store" in nested) && ("true" in nested || "false" in nested);
31
+ }
29
32
  function isOptionGroup(value) {
30
33
  return "options" in value && Array.isArray(value.options);
31
34
  }
@@ -1092,7 +1095,7 @@ function processRpcDirective(field, result, context) {
1092
1095
  return result;
1093
1096
  }
1094
1097
  function extractNestedAndDomain(field) {
1095
- const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isObject(field.nested) ? field.nested.store : field.nested;
1098
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isBooleanBranchNested(field.nested) ? void 0 : isObject(field.nested) ? field.nested.store : field.nested;
1096
1099
  const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : isObject(field.nested) && field.nested.domain ? field.nested.domain : void 0;
1097
1100
  return { nested, domain };
1098
1101
  }
@@ -1357,10 +1360,22 @@ async function validateFormanWithDomainsInternal(domains, options) {
1357
1360
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1358
1361
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1359
1362
  ) : void 0,
1360
- schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1363
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1364
+ // Same spec as `schemas`, but also on the failure path. Remote-resolved fields are
1365
+ // built before the verdict, so a rejection can name the fields it rejected instead of
1366
+ // leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
1367
+ // callers read its presence as a success signal.
1368
+ resolvedSchemas: options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1361
1369
  };
1362
1370
  }
1363
1371
  async function validateFormanValue(value, field, context) {
1372
+ if (context.registerOnly) {
1373
+ return {
1374
+ valid: true,
1375
+ errors: [],
1376
+ warnings: []
1377
+ };
1378
+ }
1364
1379
  if (isVisualType(field.type)) {
1365
1380
  return {
1366
1381
  valid: true,
@@ -1382,7 +1397,7 @@ async function validateFormanValue(value, field, context) {
1382
1397
  };
1383
1398
  }
1384
1399
  const normalizedField = normalizeFormanFieldType(field);
1385
- if (normalizedField.required && (value == null || value === "")) {
1400
+ if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
1386
1401
  return {
1387
1402
  valid: false,
1388
1403
  errors: [
@@ -1552,7 +1567,7 @@ async function handleCollectionType2(value, field, context) {
1552
1567
  continue;
1553
1568
  }
1554
1569
  if (context2.strict && !seen.has(subField2.name)) seen.add(subField2.name);
1555
- if (path.length === 0) {
1570
+ if (path.length === 0 && !context2.registerOnly) {
1556
1571
  context2.roots[context2.domain].schemaFields.push(clampFieldForSchema(subField2));
1557
1572
  }
1558
1573
  const result2 = await validateFormanValue(value[subField2.name], subField2, {
@@ -2145,7 +2160,7 @@ async function handlePrimitiveType2(value, field, context) {
2145
2160
  }
2146
2161
  const nested = extractNestedFromField(field);
2147
2162
  if (nested) {
2148
- const result = await handleNestedFields(nested, value, field, context);
2163
+ const result = FORMAN_TYPE_MAP2[field.type] === "boolean" ? await handleBooleanNestedFields(nested, value, field, context) : await handleNestedFields(nested, value, field, context);
2149
2164
  errors.push(...result.errors);
2150
2165
  warnings.push(...result.warnings);
2151
2166
  }
@@ -2155,6 +2170,19 @@ async function handlePrimitiveType2(value, field, context) {
2155
2170
  warnings
2156
2171
  };
2157
2172
  }
2173
+ async function handleBooleanNestedFields(nested, value, field, context) {
2174
+ if (isBooleanBranchNested(nested)) {
2175
+ const active2 = value === false ? nested.false : nested.true;
2176
+ const inactive = value === false ? nested.true : nested.false;
2177
+ const result = active2 ? await handleNestedFields(active2, value, field, context) : { valid: true, errors: [], warnings: [] };
2178
+ if (context.strict && inactive) {
2179
+ await handleNestedFields(inactive, value, field, { ...context, registerOnly: true });
2180
+ }
2181
+ return result;
2182
+ }
2183
+ const active = field.reversedNested === true ? value === false : value === true;
2184
+ return handleNestedFields(nested, value, field, active ? context : { ...context, suppressRequired: true });
2185
+ }
2158
2186
 
2159
2187
  // src/json.ts
2160
2188
  var JSON_PRIMITIVE_TYPE_MAP = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",