@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 +56 -1
- package/dist/index.cjs +53 -1
- package/dist/index.d.cts +46 -4
- package/dist/index.d.ts +46 -4
- package/dist/index.js +53 -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.");
|
|
@@ -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<
|
|
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<
|
|
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<
|
|
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<
|
|
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,
|