@makehq/forman-schema 1.18.0 → 2.0.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/README.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Conversion and validation utilities for Forman Schema.
4
4
 
5
+ ## v2.0.0 — validated values on every result
6
+
7
+ `validateForman` and `validateFormanWithDomains` now always return `normalizedValues` and
8
+ `appliedDefaults`. Nothing was removed or renamed and `valid`/`errors`/`warnings` are unaffected,
9
+ but a caller that deep-compares the whole result, or forwards it into a fixed-shape response, will
10
+ see two new keys — assert on the fields you care about, or drop the keys before forwarding.
11
+
12
+ Also new: `fillDefaults: 'always'`. See [Filling defaults](#filling-defaults).
13
+
5
14
  ## v1.14.0 — advanced field tracking
6
15
 
7
16
  Non-breaking minor release. New surface for working with `advanced: true` Forman fields:
@@ -137,7 +146,16 @@ Validate Forman values against a Forman Schema. Two entry points are available:
137
146
  - `validateForman(values, schema, options?)` — validate without domains.
138
147
  - `validateFormanWithDomains(domains, options?)` — validate multiple domains at once.
139
148
 
140
- Both return `{ valid: boolean, errors: { path: string, message: string }[] }`.
149
+ Both return `{ valid: boolean, errors: { path: string, message: string }[] }`, plus
150
+ `normalizedValues` (the input values per domain, with any filled defaults applied — see
151
+ [Filling defaults](#filling-defaults)) and `appliedDefaults` (what was filled, empty when
152
+ nothing was). The two are always present, so the consuming pattern is the same whether or
153
+ not default filling is enabled:
154
+
155
+ ```typescript
156
+ const { valid, errors, normalizedValues } = await validateForman(values, schema);
157
+ if (valid) persist(normalizedValues.default);
158
+ ```
141
159
 
142
160
  #### Basic validation
143
161
 
@@ -219,6 +237,43 @@ const result = await validateForman(values, schema, {
219
237
  });
220
238
  ```
221
239
 
240
+ #### Filling defaults
241
+
242
+ With `fillDefaults: 'requiredOnly'`, an omitted required field whose schema declares a usable
243
+ default (`null` and `''` cannot satisfy a required check) validates as that default instead of
244
+ failing as mandatory. With `fillDefaults: 'always'`, omitted optional fields with usable defaults
245
+ are filled too — the same modes as the platform's BlueprintValidator `useDefaults` option. The
246
+ filled value participates in the rest of the walk, so a filled boolean conditions its nested branch
247
+ exactly as a provided one would, and defaults under an armed branch fill recursively — including
248
+ fields injected by `rpc://`-resolved specs. Fills land in `normalizedValues` (the values with fills
249
+ applied; the input is never mutated, though subtrees nothing was written into are shared with it)
250
+ and are itemized in `appliedDefaults`, on the failure path too, so remaining errors can be repaired
251
+ on top of the filled values. Values you provide are never overwritten, **except `''`, which counts as
252
+ an omission and fills** — matching blueprint validation and the builder UI. Under `'always'` that
253
+ means an optional field you deliberately cleared comes back with its default; pass `'requiredOnly'`
254
+ if you need a cleared optional field left alone. An explicit `null` is a provided value: it never
255
+ fills and still fails as mandatory. Inactive nested branches are never filled.
256
+
257
+ ```typescript
258
+ const schema = [
259
+ {
260
+ name: 'fallbackEnabled',
261
+ type: 'boolean',
262
+ required: true,
263
+ default: false,
264
+ nested: [{ name: 'fallbackConnectionId', type: 'text', required: true }],
265
+ },
266
+ ];
267
+
268
+ const result = await validateForman({}, schema, { fillDefaults: 'requiredOnly' });
269
+ // {
270
+ // valid: true,
271
+ // errors: [],
272
+ // normalizedValues: { default: { fallbackEnabled: false } },
273
+ // appliedDefaults: [{ domain: 'default', path: 'fallbackEnabled', value: false }]
274
+ // }
275
+ ```
276
+
222
277
  #### Multi-domain validation
223
278
 
224
279
  Use `validateFormanWithDomains` to validate cross-domain schemas (e.g., `default` and `additional`).
package/dist/index.cjs CHANGED
@@ -115,6 +115,27 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
115
115
  const found = optionsAndGroups.find((option) => valueKey in option && matches(option));
116
116
  return found;
117
117
  }
118
+ function setIn(container, path, value) {
119
+ const [head, ...rest] = path;
120
+ if (head === void 0) return value;
121
+ if (typeof head === "number") {
122
+ const items = Array.isArray(container) ? container.slice() : [];
123
+ items[head] = setIn(items[head], rest, value);
124
+ return items;
125
+ }
126
+ const record = isObject(container) ? { ...container } : {};
127
+ record[head] = setIn(record[head], rest, value);
128
+ return record;
129
+ }
130
+ function setValueAtPath(values, path, value) {
131
+ const [head] = path;
132
+ if (typeof head !== "string") {
133
+ throw new Error(`Cannot write a value at path '${path.join(".")}': the first segment must be a field name.`);
134
+ }
135
+ const record = { ...values };
136
+ record[head] = setIn(record[head], path.slice(1), value);
137
+ return record;
138
+ }
118
139
  function pathToString(path) {
119
140
  let result = "";
120
141
  for (const key of path) {
@@ -1292,6 +1313,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1292
1313
  seenFields: /* @__PURE__ */ new Set(),
1293
1314
  fieldStates: [],
1294
1315
  schemaFields: [],
1316
+ appliedDefaults: [],
1295
1317
  allowDynamicValues: domains[domain].allowDynamicValues ?? options?.allowDynamicValues ?? false,
1296
1318
  validateFields: (fields, context) => {
1297
1319
  return validateFormanValue(
@@ -1328,6 +1350,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1328
1350
  path: [],
1329
1351
  tail: [],
1330
1352
  strict: options?.strict === true,
1353
+ fillDefaults: options?.fillDefaults,
1331
1354
  domainAliases: options?.domainAliases ?? {},
1332
1355
  validateNestedFields: () => {
1333
1356
  throw new Error("Cannot validate nested fields without parent field.");
@@ -1392,7 +1415,28 @@ async function validateFormanWithDomainsInternal(domains, options) {
1392
1415
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1393
1416
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1394
1417
  ) : void 0,
1395
- schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1418
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1419
+ // Same spec as `schemas`, but also on the failure path. Remote-resolved fields are
1420
+ // built before the verdict, so a rejection can name the fields it rejected instead of
1421
+ // leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
1422
+ // callers read its presence as a success signal.
1423
+ resolvedSchemas: options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1424
+ normalizedValues: Object.fromEntries(
1425
+ Object.keys(domains).map((domain) => [
1426
+ domain,
1427
+ (roots[domain]?.appliedDefaults ?? []).reduce(
1428
+ (values, { path, value }) => setValueAtPath(values, path, value),
1429
+ domains[domain]?.values ?? {}
1430
+ )
1431
+ ])
1432
+ ),
1433
+ appliedDefaults: Object.keys(domains).flatMap(
1434
+ (domain) => roots[domain]?.appliedDefaults.map(({ path, value }) => ({
1435
+ domain,
1436
+ path: path.join("."),
1437
+ value
1438
+ })) ?? []
1439
+ )
1396
1440
  };
1397
1441
  }
1398
1442
  async function validateFormanValue(value, field, context) {
@@ -1424,6 +1468,14 @@ async function validateFormanValue(value, field, context) {
1424
1468
  };
1425
1469
  }
1426
1470
  const normalizedField = normalizeFormanFieldType(field);
1471
+ if (context.fillDefaults != null && !context.suppressRequired && (value === void 0 || value === "") && (context.fillDefaults === "always" || normalizedField.required)) {
1472
+ const fillable = normalizedField.default;
1473
+ if (fillable != null && fillable !== "") {
1474
+ const filled = isObject(fillable) || Array.isArray(fillable) ? structuredClone(fillable) : fillable;
1475
+ value = filled;
1476
+ context.roots[context.domain].appliedDefaults.push({ path: [...context.path], value: filled });
1477
+ }
1478
+ }
1427
1479
  if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
1428
1480
  return {
1429
1481
  valid: false,
package/dist/index.d.cts CHANGED
@@ -216,9 +216,37 @@ type FormanValidationResult = {
216
216
  }[];
217
217
  /** States of fields grouped by domain */
218
218
  states?: Record<string, FormanSchemaFieldState>;
219
- /** Resolved schema fields per domain */
219
+ /** Resolved schema fields per domain. Requires `options.schemas`; only present when validation succeeded. */
220
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[]>;
228
+ /**
229
+ * The input values with filled defaults applied, per domain, so a caller can persist or repair
230
+ * the filled configuration alongside any remaining errors. The input `values` are never
231
+ * mutated, but subtrees no default was written into are shared with the input — with no fills
232
+ * the domain's entry IS the caller's own object.
233
+ */
234
+ normalizedValues?: Record<string, Record<string, unknown>>;
235
+ /** The defaults that were filled (`options.fillDefaults`), in walk order within each domain. */
236
+ appliedDefaults?: {
237
+ domain: string;
238
+ path: string;
239
+ /** The default that was filled in. Loosely typed on purpose: `default` is declared as
240
+ * `FormanSchemaValue`, but schemas are JSON at source and may carry object or array
241
+ * defaults at runtime, which are filled as (cloned) values too. */
242
+ value: unknown;
243
+ }[];
221
244
  };
245
+ /**
246
+ * The type returned by `validateForman` and `validateFormanWithDomains`. The fields stay optional
247
+ * on the base type because intermediate results assembled during the walk do not carry them.
248
+ */
249
+ type FormanNormalizedValidationResult = FormanValidationResult & Required<Pick<FormanValidationResult, 'normalizedValues' | 'appliedDefaults'>>;
222
250
  type FormanSchemaFieldState = {
223
251
  mode?: 'chose' | 'edit';
224
252
  label?: string;
@@ -297,6 +325,20 @@ type FormanValidationOptions = {
297
325
  * returns (or resolves to) a result fragment that is spliced into the overall validation
298
326
  * result. When omitted, `json` fields with a `schema` pass without schema validation. */
299
327
  validateJson?(schema: JSONSchema7, value: unknown): FormanExternalValidationResult | Promise<FormanExternalValidationResult>;
328
+ /** Fill declared defaults for fields the caller omitted, mirroring BlueprintValidator's
329
+ * `useDefaults` modes. `'requiredOnly'` fills only required fields (instead of failing them
330
+ * as mandatory); `'always'` also fills omitted optional fields. A field is fillable when its
331
+ * value is `undefined` or `''` and its schema declares a default that is not `null` or `''`
332
+ * (a default that could not satisfy a required check) — the same fillable predicate as
333
+ * BlueprintValidator and the builder UI. The filled value flows through the rest of the
334
+ * walk, so a filled boolean conditions its nested branch exactly as a provided one would
335
+ * (`false` leaves the branch inactive), and defaults under an armed branch fill
336
+ * recursively, in the same single pass. Fills are reported on `normalizedValues` and
337
+ * `appliedDefaults`. Values the caller provided are never overwritten, except `''`, which
338
+ * counts as an omission and fills — so under `'always'` a deliberately cleared optional
339
+ * field comes back with its default. An explicit `null` is a provided value: it never fills
340
+ * and still fails as mandatory. Inactive branches are never filled. */
341
+ fillDefaults?: 'requiredOnly' | 'always';
300
342
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
301
343
  domainAliases?: Record<string, string>;
302
344
  /** Whether to allow dynamic values (IML expressions, unresolved RPC options).
@@ -393,7 +435,7 @@ declare function validateFormanWithDomains(domains: Record<string, {
393
435
  /** Whether the domain allows dynamic values (IML expressions, unresolved RPC select options).
394
436
  * Defaults to false. When false, IML expressions cause errors and unresolved RPC options are treated as errors. */
395
437
  allowDynamicValues?: boolean;
396
- }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
438
+ }>, options?: FormanValidationOptions): Promise<FormanNormalizedValidationResult>;
397
439
  /**
398
440
  * Validates a simple Forman values against a schema
399
441
  * @param values The values to validate
@@ -404,6 +446,6 @@ declare function validateFormanWithDomains(domains: Record<string, {
404
446
  * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
405
447
  * @returns The validation result
406
448
  */
407
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
449
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanNormalizedValidationResult>;
408
450
 
409
- 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 };
451
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanNormalizedValidationResult, 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
@@ -216,9 +216,37 @@ type FormanValidationResult = {
216
216
  }[];
217
217
  /** States of fields grouped by domain */
218
218
  states?: Record<string, FormanSchemaFieldState>;
219
- /** Resolved schema fields per domain */
219
+ /** Resolved schema fields per domain. Requires `options.schemas`; only present when validation succeeded. */
220
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[]>;
228
+ /**
229
+ * The input values with filled defaults applied, per domain, so a caller can persist or repair
230
+ * the filled configuration alongside any remaining errors. The input `values` are never
231
+ * mutated, but subtrees no default was written into are shared with the input — with no fills
232
+ * the domain's entry IS the caller's own object.
233
+ */
234
+ normalizedValues?: Record<string, Record<string, unknown>>;
235
+ /** The defaults that were filled (`options.fillDefaults`), in walk order within each domain. */
236
+ appliedDefaults?: {
237
+ domain: string;
238
+ path: string;
239
+ /** The default that was filled in. Loosely typed on purpose: `default` is declared as
240
+ * `FormanSchemaValue`, but schemas are JSON at source and may carry object or array
241
+ * defaults at runtime, which are filled as (cloned) values too. */
242
+ value: unknown;
243
+ }[];
221
244
  };
245
+ /**
246
+ * The type returned by `validateForman` and `validateFormanWithDomains`. The fields stay optional
247
+ * on the base type because intermediate results assembled during the walk do not carry them.
248
+ */
249
+ type FormanNormalizedValidationResult = FormanValidationResult & Required<Pick<FormanValidationResult, 'normalizedValues' | 'appliedDefaults'>>;
222
250
  type FormanSchemaFieldState = {
223
251
  mode?: 'chose' | 'edit';
224
252
  label?: string;
@@ -297,6 +325,20 @@ type FormanValidationOptions = {
297
325
  * returns (or resolves to) a result fragment that is spliced into the overall validation
298
326
  * result. When omitted, `json` fields with a `schema` pass without schema validation. */
299
327
  validateJson?(schema: JSONSchema7, value: unknown): FormanExternalValidationResult | Promise<FormanExternalValidationResult>;
328
+ /** Fill declared defaults for fields the caller omitted, mirroring BlueprintValidator's
329
+ * `useDefaults` modes. `'requiredOnly'` fills only required fields (instead of failing them
330
+ * as mandatory); `'always'` also fills omitted optional fields. A field is fillable when its
331
+ * value is `undefined` or `''` and its schema declares a default that is not `null` or `''`
332
+ * (a default that could not satisfy a required check) — the same fillable predicate as
333
+ * BlueprintValidator and the builder UI. The filled value flows through the rest of the
334
+ * walk, so a filled boolean conditions its nested branch exactly as a provided one would
335
+ * (`false` leaves the branch inactive), and defaults under an armed branch fill
336
+ * recursively, in the same single pass. Fills are reported on `normalizedValues` and
337
+ * `appliedDefaults`. Values the caller provided are never overwritten, except `''`, which
338
+ * counts as an omission and fills — so under `'always'` a deliberately cleared optional
339
+ * field comes back with its default. An explicit `null` is a provided value: it never fills
340
+ * and still fails as mandatory. Inactive branches are never filled. */
341
+ fillDefaults?: 'requiredOnly' | 'always';
300
342
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
301
343
  domainAliases?: Record<string, string>;
302
344
  /** Whether to allow dynamic values (IML expressions, unresolved RPC options).
@@ -393,7 +435,7 @@ declare function validateFormanWithDomains(domains: Record<string, {
393
435
  /** Whether the domain allows dynamic values (IML expressions, unresolved RPC select options).
394
436
  * Defaults to false. When false, IML expressions cause errors and unresolved RPC options are treated as errors. */
395
437
  allowDynamicValues?: boolean;
396
- }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
438
+ }>, options?: FormanValidationOptions): Promise<FormanNormalizedValidationResult>;
397
439
  /**
398
440
  * Validates a simple Forman values against a schema
399
441
  * @param values The values to validate
@@ -404,6 +446,6 @@ declare function validateFormanWithDomains(domains: Record<string, {
404
446
  * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
405
447
  * @returns The validation result
406
448
  */
407
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
449
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanNormalizedValidationResult>;
408
450
 
409
- 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 };
451
+ export { type FormanExternalValidationResult, type FormanJsonSchemaOptions, type FormanJsonSchemaResult, type FormanNormalizedValidationResult, 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
@@ -83,6 +83,27 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
83
83
  const found = optionsAndGroups.find((option) => valueKey in option && matches(option));
84
84
  return found;
85
85
  }
86
+ function setIn(container, path, value) {
87
+ const [head, ...rest] = path;
88
+ if (head === void 0) return value;
89
+ if (typeof head === "number") {
90
+ const items = Array.isArray(container) ? container.slice() : [];
91
+ items[head] = setIn(items[head], rest, value);
92
+ return items;
93
+ }
94
+ const record = isObject(container) ? { ...container } : {};
95
+ record[head] = setIn(record[head], rest, value);
96
+ return record;
97
+ }
98
+ function setValueAtPath(values, path, value) {
99
+ const [head] = path;
100
+ if (typeof head !== "string") {
101
+ throw new Error(`Cannot write a value at path '${path.join(".")}': the first segment must be a field name.`);
102
+ }
103
+ const record = { ...values };
104
+ record[head] = setIn(record[head], path.slice(1), value);
105
+ return record;
106
+ }
86
107
  function pathToString(path) {
87
108
  let result = "";
88
109
  for (const key of path) {
@@ -1260,6 +1281,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1260
1281
  seenFields: /* @__PURE__ */ new Set(),
1261
1282
  fieldStates: [],
1262
1283
  schemaFields: [],
1284
+ appliedDefaults: [],
1263
1285
  allowDynamicValues: domains[domain].allowDynamicValues ?? options?.allowDynamicValues ?? false,
1264
1286
  validateFields: (fields, context) => {
1265
1287
  return validateFormanValue(
@@ -1296,6 +1318,7 @@ async function validateFormanWithDomainsInternal(domains, options) {
1296
1318
  path: [],
1297
1319
  tail: [],
1298
1320
  strict: options?.strict === true,
1321
+ fillDefaults: options?.fillDefaults,
1299
1322
  domainAliases: options?.domainAliases ?? {},
1300
1323
  validateNestedFields: () => {
1301
1324
  throw new Error("Cannot validate nested fields without parent field.");
@@ -1360,7 +1383,28 @@ async function validateFormanWithDomainsInternal(domains, options) {
1360
1383
  states: errors.length === 0 && options?.states ? buildRestoreStructure(
1361
1384
  Object.keys(domains).map((domain) => roots[domain].fieldStates.map((state) => ({ domain, ...state }))).flat()
1362
1385
  ) : void 0,
1363
- schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0
1386
+ schemas: errors.length === 0 && options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1387
+ // Same spec as `schemas`, but also on the failure path. Remote-resolved fields are
1388
+ // built before the verdict, so a rejection can name the fields it rejected instead of
1389
+ // leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
1390
+ // callers read its presence as a success signal.
1391
+ resolvedSchemas: options?.schemas ? Object.fromEntries(Object.keys(domains).map((domain) => [domain, roots[domain].schemaFields])) : void 0,
1392
+ normalizedValues: Object.fromEntries(
1393
+ Object.keys(domains).map((domain) => [
1394
+ domain,
1395
+ (roots[domain]?.appliedDefaults ?? []).reduce(
1396
+ (values, { path, value }) => setValueAtPath(values, path, value),
1397
+ domains[domain]?.values ?? {}
1398
+ )
1399
+ ])
1400
+ ),
1401
+ appliedDefaults: Object.keys(domains).flatMap(
1402
+ (domain) => roots[domain]?.appliedDefaults.map(({ path, value }) => ({
1403
+ domain,
1404
+ path: path.join("."),
1405
+ value
1406
+ })) ?? []
1407
+ )
1364
1408
  };
1365
1409
  }
1366
1410
  async function validateFormanValue(value, field, context) {
@@ -1392,6 +1436,14 @@ async function validateFormanValue(value, field, context) {
1392
1436
  };
1393
1437
  }
1394
1438
  const normalizedField = normalizeFormanFieldType(field);
1439
+ if (context.fillDefaults != null && !context.suppressRequired && (value === void 0 || value === "") && (context.fillDefaults === "always" || normalizedField.required)) {
1440
+ const fillable = normalizedField.default;
1441
+ if (fillable != null && fillable !== "") {
1442
+ const filled = isObject(fillable) || Array.isArray(fillable) ? structuredClone(fillable) : fillable;
1443
+ value = filled;
1444
+ context.roots[context.domain].appliedDefaults.push({ path: [...context.path], value: filled });
1445
+ }
1446
+ }
1395
1447
  if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
1396
1448
  return {
1397
1449
  valid: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.18.0",
3
+ "version": "2.0.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",