@makehq/forman-schema 1.19.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 +56 -1
- package/dist/index.cjs +48 -1
- package/dist/index.d.cts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +48 -1
- package/package.json +1 -1
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.");
|
|
@@ -1397,7 +1420,23 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
1397
1420
|
// built before the verdict, so a rejection can name the fields it rejected instead of
|
|
1398
1421
|
// leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
|
|
1399
1422
|
// 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
|
|
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
|
+
)
|
|
1401
1440
|
};
|
|
1402
1441
|
}
|
|
1403
1442
|
async function validateFormanValue(value, field, context) {
|
|
@@ -1429,6 +1468,14 @@ async function validateFormanValue(value, field, context) {
|
|
|
1429
1468
|
};
|
|
1430
1469
|
}
|
|
1431
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
|
+
}
|
|
1432
1479
|
if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
|
|
1433
1480
|
return {
|
|
1434
1481
|
valid: false,
|
package/dist/index.d.cts
CHANGED
|
@@ -225,7 +225,28 @@ type FormanValidationResult = {
|
|
|
225
225
|
* which fields it rejected.
|
|
226
226
|
*/
|
|
227
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
|
+
}[];
|
|
228
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'>>;
|
|
229
250
|
type FormanSchemaFieldState = {
|
|
230
251
|
mode?: 'chose' | 'edit';
|
|
231
252
|
label?: string;
|
|
@@ -304,6 +325,20 @@ type FormanValidationOptions = {
|
|
|
304
325
|
* returns (or resolves to) a result fragment that is spliced into the overall validation
|
|
305
326
|
* result. When omitted, `json` fields with a `schema` pass without schema validation. */
|
|
306
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';
|
|
307
342
|
/** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
|
|
308
343
|
domainAliases?: Record<string, string>;
|
|
309
344
|
/** Whether to allow dynamic values (IML expressions, unresolved RPC options).
|
|
@@ -400,7 +435,7 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
400
435
|
/** Whether the domain allows dynamic values (IML expressions, unresolved RPC select options).
|
|
401
436
|
* Defaults to false. When false, IML expressions cause errors and unresolved RPC options are treated as errors. */
|
|
402
437
|
allowDynamicValues?: boolean;
|
|
403
|
-
}>, options?: FormanValidationOptions): Promise<
|
|
438
|
+
}>, options?: FormanValidationOptions): Promise<FormanNormalizedValidationResult>;
|
|
404
439
|
/**
|
|
405
440
|
* Validates a simple Forman values against a schema
|
|
406
441
|
* @param values The values to validate
|
|
@@ -411,6 +446,6 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
411
446
|
* and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
|
|
412
447
|
* @returns The validation result
|
|
413
448
|
*/
|
|
414
|
-
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<
|
|
449
|
+
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanNormalizedValidationResult>;
|
|
415
450
|
|
|
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 };
|
|
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
|
@@ -225,7 +225,28 @@ type FormanValidationResult = {
|
|
|
225
225
|
* which fields it rejected.
|
|
226
226
|
*/
|
|
227
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
|
+
}[];
|
|
228
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'>>;
|
|
229
250
|
type FormanSchemaFieldState = {
|
|
230
251
|
mode?: 'chose' | 'edit';
|
|
231
252
|
label?: string;
|
|
@@ -304,6 +325,20 @@ type FormanValidationOptions = {
|
|
|
304
325
|
* returns (or resolves to) a result fragment that is spliced into the overall validation
|
|
305
326
|
* result. When omitted, `json` fields with a `schema` pass without schema validation. */
|
|
306
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';
|
|
307
342
|
/** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
|
|
308
343
|
domainAliases?: Record<string, string>;
|
|
309
344
|
/** Whether to allow dynamic values (IML expressions, unresolved RPC options).
|
|
@@ -400,7 +435,7 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
400
435
|
/** Whether the domain allows dynamic values (IML expressions, unresolved RPC select options).
|
|
401
436
|
* Defaults to false. When false, IML expressions cause errors and unresolved RPC options are treated as errors. */
|
|
402
437
|
allowDynamicValues?: boolean;
|
|
403
|
-
}>, options?: FormanValidationOptions): Promise<
|
|
438
|
+
}>, options?: FormanValidationOptions): Promise<FormanNormalizedValidationResult>;
|
|
404
439
|
/**
|
|
405
440
|
* Validates a simple Forman values against a schema
|
|
406
441
|
* @param values The values to validate
|
|
@@ -411,6 +446,6 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
411
446
|
* and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
|
|
412
447
|
* @returns The validation result
|
|
413
448
|
*/
|
|
414
|
-
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<
|
|
449
|
+
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanNormalizedValidationResult>;
|
|
415
450
|
|
|
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 };
|
|
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.");
|
|
@@ -1365,7 +1388,23 @@ async function validateFormanWithDomainsInternal(domains, options) {
|
|
|
1365
1388
|
// built before the verdict, so a rejection can name the fields it rejected instead of
|
|
1366
1389
|
// leaving the caller to guess at a sub-form it never saw. `schemas` stays success-only:
|
|
1367
1390
|
// 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
|
|
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
|
+
)
|
|
1369
1408
|
};
|
|
1370
1409
|
}
|
|
1371
1410
|
async function validateFormanValue(value, field, context) {
|
|
@@ -1397,6 +1436,14 @@ async function validateFormanValue(value, field, context) {
|
|
|
1397
1436
|
};
|
|
1398
1437
|
}
|
|
1399
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
|
+
}
|
|
1400
1447
|
if (normalizedField.required && !context.suppressRequired && (value == null || value === "")) {
|
|
1401
1448
|
return {
|
|
1402
1449
|
valid: false,
|