@makehq/forman-schema 1.16.0 → 1.17.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 +43 -1
- package/dist/index.cjs +69 -10
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +67 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -291,11 +291,53 @@ const result = await validateFormanWithDomains(
|
|
|
291
291
|
- object → collection
|
|
292
292
|
- array → array
|
|
293
293
|
|
|
294
|
+
## Field type resolution
|
|
295
|
+
|
|
296
|
+
Field types resolve through three steps, so schemas authored with loose casing or common synonyms
|
|
297
|
+
still convert:
|
|
298
|
+
|
|
299
|
+
1. **Exact match** against `FORMAN_TYPE_MAP`.
|
|
300
|
+
2. **Case-insensitive match** — `fileName`, `Boolean`, `URL`, `Select` resolve to their canonical
|
|
301
|
+
lowercase types. The index is derived from the map itself, so new entries get this for free.
|
|
302
|
+
3. **Aliases** — `string→text`, `bool→boolean`, `datetime→date`, `float→number`,
|
|
303
|
+
`upload→filestorage`. Only unambiguous, information-preserving synonyms are aliased.
|
|
304
|
+
|
|
305
|
+
A `type:kind` suffix (`account:google`, `device:apn`) resolves on its base type and keeps the kind,
|
|
306
|
+
which drives the `api://` store expansion.
|
|
307
|
+
|
|
308
|
+
### Unconvertible fields
|
|
309
|
+
|
|
310
|
+
A field whose type is **missing or unresolvable** is degraded to a permissive typeless schema (the
|
|
311
|
+
same shape `any` produces) instead of aborting the conversion, and its dot-notation path is reported
|
|
312
|
+
on `toJSONSchemaAdvanced`'s `skippedPaths.unconvertible`:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
const { schema, skippedPaths } = toJSONSchemaAdvanced({
|
|
316
|
+
name: 'wrapper',
|
|
317
|
+
type: 'collection',
|
|
318
|
+
spec: [
|
|
319
|
+
{ name: 'good', type: 'text' },
|
|
320
|
+
{ name: 'odd', type: 'somethingNew' },
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
// schema.properties → { good: { type: 'string' }, odd: {} }
|
|
324
|
+
// skippedPaths → { unconvertible: ['wrapper.odd (unknown type: somethingNew)'] }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
This is deliberate: the throw was fatal at any depth, so a single unrecognized leaf field destroyed
|
|
328
|
+
the whole schema and left consumers with nothing. Types requiring a guess about intent (`tags`,
|
|
329
|
+
`category`, `object`) are degraded rather than aliased — a degraded field is honest, a wrongly
|
|
330
|
+
aliased one is a lie the consumer will act on.
|
|
331
|
+
|
|
332
|
+
Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
|
|
333
|
+
|
|
294
334
|
## Error Handling
|
|
295
335
|
|
|
296
336
|
### SchemaConversionError
|
|
297
337
|
|
|
298
|
-
`SchemaConversionError` is thrown when schema conversion fails
|
|
338
|
+
`SchemaConversionError` is thrown when schema conversion fails, and for unresolvable field types
|
|
339
|
+
only when `strictFieldTypes: true` is set. It carries a message and the `field` that caused the
|
|
340
|
+
error.
|
|
299
341
|
|
|
300
342
|
## Testing
|
|
301
343
|
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
SchemaConversionError: () => SchemaConversionError,
|
|
24
|
+
resolveFormanFieldType: () => resolveFormanFieldType,
|
|
23
25
|
toFormanSchema: () => toFormanSchema,
|
|
24
26
|
toJSONSchema: () => toJSONSchema,
|
|
25
27
|
toJSONSchemaAdvanced: () => toJSONSchemaAdvanced,
|
|
@@ -543,11 +545,13 @@ var SchemaConversionError = class extends Error {
|
|
|
543
545
|
constructor(message, field) {
|
|
544
546
|
super(message);
|
|
545
547
|
this.name = "SchemaConversionError";
|
|
548
|
+
this.field = field;
|
|
546
549
|
}
|
|
547
550
|
};
|
|
548
551
|
var FORMAN_TYPE_MAP = {
|
|
549
552
|
account: "number",
|
|
550
553
|
hook: "number",
|
|
554
|
+
device: "number",
|
|
551
555
|
keychain: "number",
|
|
552
556
|
datastore: "number",
|
|
553
557
|
aiagent: "string",
|
|
@@ -591,14 +595,29 @@ var FORMAN_TYPE_MAP = {
|
|
|
591
595
|
uuid: "string",
|
|
592
596
|
any: void 0
|
|
593
597
|
};
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
598
|
+
var FORMAN_TYPE_LOOKUP = new Map(
|
|
599
|
+
Object.keys(FORMAN_TYPE_MAP).map((key) => [key.toLowerCase(), key])
|
|
600
|
+
);
|
|
601
|
+
var FORMAN_TYPE_ALIASES = {
|
|
602
|
+
// Forman's plain-string primitive is `text`; there is no `string` type.
|
|
603
|
+
string: "text",
|
|
604
|
+
bool: "boolean",
|
|
605
|
+
// Forman's `date` already carries time (see `time` on FormanSchemaField, default true).
|
|
606
|
+
datetime: "date",
|
|
607
|
+
// JSON Schema has no float; `number` is the only target.
|
|
608
|
+
float: "number",
|
|
609
|
+
// `upload` is the validator's existing name for the same array-shaped type.
|
|
610
|
+
upload: "filestorage"
|
|
611
|
+
};
|
|
612
|
+
function resolveFormanFieldType(rawType) {
|
|
613
|
+
if (!rawType) return void 0;
|
|
614
|
+
const base = rawType.includes(":") ? rawType.split(":")[0] : rawType;
|
|
615
|
+
const lower = base.toLowerCase();
|
|
616
|
+
return FORMAN_TYPE_LOOKUP.get(lower) ?? FORMAN_TYPE_ALIASES[lower];
|
|
617
|
+
}
|
|
618
|
+
function canonicalTypeWithKind(rawType, canonicalType) {
|
|
619
|
+
const separatorIndex = rawType.indexOf(":");
|
|
620
|
+
return separatorIndex === -1 ? canonicalType : `${canonicalType}${rawType.slice(separatorIndex)}`;
|
|
602
621
|
}
|
|
603
622
|
function appendQueryString(path, domain, tail) {
|
|
604
623
|
if (path.startsWith("api://")) return path;
|
|
@@ -616,6 +635,7 @@ function createDefaultContext(options) {
|
|
|
616
635
|
roots: {},
|
|
617
636
|
definitions: {},
|
|
618
637
|
excludeAdvancedFields: options?.excludeAdvancedFields ?? false,
|
|
638
|
+
strictFieldTypes: options?.strictFieldTypes ?? false,
|
|
619
639
|
skippedPaths: {},
|
|
620
640
|
addConditionalFields: () => {
|
|
621
641
|
throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
|
|
@@ -627,8 +647,19 @@ var compositeHandlers = {
|
|
|
627
647
|
udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
|
|
628
648
|
};
|
|
629
649
|
function toJSONSchemaInternal(field, context) {
|
|
630
|
-
|
|
631
|
-
|
|
650
|
+
const canonicalType = resolveFormanFieldType(field.type);
|
|
651
|
+
if (!canonicalType) {
|
|
652
|
+
if (context.strictFieldTypes) {
|
|
653
|
+
throw new SchemaConversionError(
|
|
654
|
+
field.type ? `Unknown field type: ${field.type}` : "Field type is required",
|
|
655
|
+
field
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return degradeUnconvertibleField(field, context);
|
|
659
|
+
}
|
|
660
|
+
const normalizedField = normalizeFormanFieldType(
|
|
661
|
+
canonicalType === field.type ? field : { ...field, type: canonicalTypeWithKind(field.type, canonicalType) }
|
|
662
|
+
);
|
|
632
663
|
const handler = compositeHandlers[normalizedField.type];
|
|
633
664
|
if (handler) {
|
|
634
665
|
const type = normalizedField.type;
|
|
@@ -1043,6 +1074,16 @@ function handleJsonType(field, result) {
|
|
|
1043
1074
|
});
|
|
1044
1075
|
return result;
|
|
1045
1076
|
}
|
|
1077
|
+
function degradeUnconvertibleField(field, context) {
|
|
1078
|
+
const path = field.name ? [...context.path, field.name].join(".") : context.path.join(".");
|
|
1079
|
+
const reason = field.type ? `unknown type: ${field.type}` : "missing type";
|
|
1080
|
+
(context.skippedPaths.unconvertible ||= []).push(`${path} (${reason})`);
|
|
1081
|
+
return {
|
|
1082
|
+
type: FORMAN_TYPE_MAP["any"],
|
|
1083
|
+
title: noEmpty(field.label),
|
|
1084
|
+
description: noEmpty(field.help)
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1046
1087
|
function handlePrimitiveType(field, result, context) {
|
|
1047
1088
|
if (field.default !== "" && field.default != null) {
|
|
1048
1089
|
result.default = field.default;
|
|
@@ -1359,6 +1400,19 @@ async function validateFormanValue(value, field, context) {
|
|
|
1359
1400
|
warnings: []
|
|
1360
1401
|
};
|
|
1361
1402
|
}
|
|
1403
|
+
if (!field.type) {
|
|
1404
|
+
return {
|
|
1405
|
+
valid: false,
|
|
1406
|
+
errors: [
|
|
1407
|
+
{
|
|
1408
|
+
domain: context.domain,
|
|
1409
|
+
path: context.path.join("."),
|
|
1410
|
+
message: "Field type is required."
|
|
1411
|
+
}
|
|
1412
|
+
],
|
|
1413
|
+
warnings: []
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1362
1416
|
const normalizedField = normalizeFormanFieldType(field);
|
|
1363
1417
|
if (normalizedField.required && (value == null || value === "")) {
|
|
1364
1418
|
return {
|
|
@@ -2304,6 +2358,9 @@ function toJSONSchemaAdvanced(field, options) {
|
|
|
2304
2358
|
if (context.skippedPaths.advanced?.length) {
|
|
2305
2359
|
skippedPaths.advanced = context.skippedPaths.advanced;
|
|
2306
2360
|
}
|
|
2361
|
+
if (context.skippedPaths.unconvertible?.length) {
|
|
2362
|
+
skippedPaths.unconvertible = context.skippedPaths.unconvertible;
|
|
2363
|
+
}
|
|
2307
2364
|
return {
|
|
2308
2365
|
schema,
|
|
2309
2366
|
...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
|
|
@@ -2323,6 +2380,8 @@ function validateForman(values, schema, options, restoreExtras) {
|
|
|
2323
2380
|
}
|
|
2324
2381
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2325
2382
|
0 && (module.exports = {
|
|
2383
|
+
SchemaConversionError,
|
|
2384
|
+
resolveFormanFieldType,
|
|
2326
2385
|
toFormanSchema,
|
|
2327
2386
|
toJSONSchema,
|
|
2328
2387
|
toJSONSchemaAdvanced,
|
package/dist/index.d.cts
CHANGED
|
@@ -240,6 +240,14 @@ type FormanJsonSchemaOptions = {
|
|
|
240
240
|
* `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
|
|
241
241
|
*/
|
|
242
242
|
excludeAdvancedFields?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Throw a `SchemaConversionError` when a field's type cannot be resolved, instead of degrading
|
|
245
|
+
* it to a permissive typeless schema. Defaults to `false` — by default, unresolvable fields are
|
|
246
|
+
* degraded and their dot-notation paths reported on `toJSONSchemaAdvanced`'s
|
|
247
|
+
* `skippedPaths.unconvertible`, so one unrecognized field can no longer abort the whole
|
|
248
|
+
* conversion. Set `true` only when a caller genuinely wants fail-fast behaviour.
|
|
249
|
+
*/
|
|
250
|
+
strictFieldTypes?: boolean;
|
|
243
251
|
};
|
|
244
252
|
/**
|
|
245
253
|
* Result of converting a Forman Schema to JSON Schema
|
|
@@ -251,6 +259,12 @@ type FormanJsonSchemaResult = {
|
|
|
251
259
|
skippedPaths?: {
|
|
252
260
|
/** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
|
|
253
261
|
advanced?: string[];
|
|
262
|
+
/**
|
|
263
|
+
* Dot-notation paths of fields whose type could not be resolved and were therefore degraded
|
|
264
|
+
* to a permissive typeless schema. Each entry is suffixed with the reason —
|
|
265
|
+
* `(unknown type: X)` or `(missing type)`. Present only when at least one field was degraded.
|
|
266
|
+
*/
|
|
267
|
+
unconvertible?: string[];
|
|
254
268
|
};
|
|
255
269
|
};
|
|
256
270
|
/**
|
|
@@ -294,6 +308,26 @@ type FormanValidationOptions = {
|
|
|
294
308
|
*/
|
|
295
309
|
declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
|
|
296
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Error thrown when schema conversion fails
|
|
313
|
+
*/
|
|
314
|
+
declare class SchemaConversionError extends Error {
|
|
315
|
+
/** Field that caused the error */
|
|
316
|
+
readonly field?: FormanSchemaField | JSONSchema7;
|
|
317
|
+
/**
|
|
318
|
+
* @param message The error message
|
|
319
|
+
* @param field The field that caused the error
|
|
320
|
+
*/
|
|
321
|
+
constructor(message: string, field?: FormanSchemaField | JSONSchema7);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves a raw Forman field type to a canonical `FORMAN_TYPE_MAP` key, handling `type:kind`
|
|
325
|
+
* prefixes, casing and known aliases.
|
|
326
|
+
* @param rawType The raw field type from the Forman schema
|
|
327
|
+
* @returns The canonical type key, or `undefined` when the type is missing or unresolvable
|
|
328
|
+
*/
|
|
329
|
+
declare function resolveFormanFieldType(rawType: string | undefined): string | undefined;
|
|
330
|
+
|
|
297
331
|
/**
|
|
298
332
|
* Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
|
|
299
333
|
* fields that were skipped during conversion.
|
|
@@ -311,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
|
|
|
311
345
|
* `definitions[type]`; advanced fields inside a composite template are recorded with the
|
|
312
346
|
* path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
|
|
313
347
|
*
|
|
348
|
+
* **Unresolvable field types** are tolerated by default. A field whose type is missing, or is not a
|
|
349
|
+
* known type (after case-insensitive and alias resolution), is degraded to a permissive typeless
|
|
350
|
+
* schema rather than aborting the whole conversion, and its dot-notation path is reported on
|
|
351
|
+
* `skippedPaths.unconvertible` with the reason. This matters because the throw was fatal at any
|
|
352
|
+
* depth: a single unrecognized leaf field destroyed the entire schema, leaving consumers with
|
|
353
|
+
* nothing. Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
|
|
354
|
+
*
|
|
314
355
|
* If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
|
|
315
356
|
*
|
|
316
357
|
* @param field The Forman Schema field to convert
|
|
@@ -326,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
|
|
|
326
367
|
* fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
|
|
327
368
|
* which returns `{ schema, skippedPaths? }`.
|
|
328
369
|
*
|
|
370
|
+
* Fields with an unresolvable type are degraded to a permissive schema rather than throwing; use
|
|
371
|
+
* {@link toJSONSchemaAdvanced} to see which, or `{ strictFieldTypes: true }` to throw instead.
|
|
372
|
+
*
|
|
329
373
|
* @param field The Forman Schema field to convert
|
|
330
374
|
* @param options Conversion options
|
|
331
375
|
* @returns The equivalent JSON Schema
|
|
@@ -358,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
358
402
|
*/
|
|
359
403
|
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
|
|
360
404
|
|
|
361
|
-
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, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -240,6 +240,14 @@ type FormanJsonSchemaOptions = {
|
|
|
240
240
|
* `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
|
|
241
241
|
*/
|
|
242
242
|
excludeAdvancedFields?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Throw a `SchemaConversionError` when a field's type cannot be resolved, instead of degrading
|
|
245
|
+
* it to a permissive typeless schema. Defaults to `false` — by default, unresolvable fields are
|
|
246
|
+
* degraded and their dot-notation paths reported on `toJSONSchemaAdvanced`'s
|
|
247
|
+
* `skippedPaths.unconvertible`, so one unrecognized field can no longer abort the whole
|
|
248
|
+
* conversion. Set `true` only when a caller genuinely wants fail-fast behaviour.
|
|
249
|
+
*/
|
|
250
|
+
strictFieldTypes?: boolean;
|
|
243
251
|
};
|
|
244
252
|
/**
|
|
245
253
|
* Result of converting a Forman Schema to JSON Schema
|
|
@@ -251,6 +259,12 @@ type FormanJsonSchemaResult = {
|
|
|
251
259
|
skippedPaths?: {
|
|
252
260
|
/** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
|
|
253
261
|
advanced?: string[];
|
|
262
|
+
/**
|
|
263
|
+
* Dot-notation paths of fields whose type could not be resolved and were therefore degraded
|
|
264
|
+
* to a permissive typeless schema. Each entry is suffixed with the reason —
|
|
265
|
+
* `(unknown type: X)` or `(missing type)`. Present only when at least one field was degraded.
|
|
266
|
+
*/
|
|
267
|
+
unconvertible?: string[];
|
|
254
268
|
};
|
|
255
269
|
};
|
|
256
270
|
/**
|
|
@@ -294,6 +308,26 @@ type FormanValidationOptions = {
|
|
|
294
308
|
*/
|
|
295
309
|
declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
|
|
296
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Error thrown when schema conversion fails
|
|
313
|
+
*/
|
|
314
|
+
declare class SchemaConversionError extends Error {
|
|
315
|
+
/** Field that caused the error */
|
|
316
|
+
readonly field?: FormanSchemaField | JSONSchema7;
|
|
317
|
+
/**
|
|
318
|
+
* @param message The error message
|
|
319
|
+
* @param field The field that caused the error
|
|
320
|
+
*/
|
|
321
|
+
constructor(message: string, field?: FormanSchemaField | JSONSchema7);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Resolves a raw Forman field type to a canonical `FORMAN_TYPE_MAP` key, handling `type:kind`
|
|
325
|
+
* prefixes, casing and known aliases.
|
|
326
|
+
* @param rawType The raw field type from the Forman schema
|
|
327
|
+
* @returns The canonical type key, or `undefined` when the type is missing or unresolvable
|
|
328
|
+
*/
|
|
329
|
+
declare function resolveFormanFieldType(rawType: string | undefined): string | undefined;
|
|
330
|
+
|
|
297
331
|
/**
|
|
298
332
|
* Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
|
|
299
333
|
* fields that were skipped during conversion.
|
|
@@ -311,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
|
|
|
311
345
|
* `definitions[type]`; advanced fields inside a composite template are recorded with the
|
|
312
346
|
* path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
|
|
313
347
|
*
|
|
348
|
+
* **Unresolvable field types** are tolerated by default. A field whose type is missing, or is not a
|
|
349
|
+
* known type (after case-insensitive and alias resolution), is degraded to a permissive typeless
|
|
350
|
+
* schema rather than aborting the whole conversion, and its dot-notation path is reported on
|
|
351
|
+
* `skippedPaths.unconvertible` with the reason. This matters because the throw was fatal at any
|
|
352
|
+
* depth: a single unrecognized leaf field destroyed the entire schema, leaving consumers with
|
|
353
|
+
* nothing. Pass `{ strictFieldTypes: true }` to restore fail-fast throwing.
|
|
354
|
+
*
|
|
314
355
|
* If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
|
|
315
356
|
*
|
|
316
357
|
* @param field The Forman Schema field to convert
|
|
@@ -326,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
|
|
|
326
367
|
* fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
|
|
327
368
|
* which returns `{ schema, skippedPaths? }`.
|
|
328
369
|
*
|
|
370
|
+
* Fields with an unresolvable type are degraded to a permissive schema rather than throwing; use
|
|
371
|
+
* {@link toJSONSchemaAdvanced} to see which, or `{ strictFieldTypes: true }` to throw instead.
|
|
372
|
+
*
|
|
329
373
|
* @param field The Forman Schema field to convert
|
|
330
374
|
* @param options Conversion options
|
|
331
375
|
* @returns The equivalent JSON Schema
|
|
@@ -358,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
|
|
|
358
402
|
*/
|
|
359
403
|
declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
|
|
360
404
|
|
|
361
|
-
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, toFormanSchema, toJSONSchema, toJSONSchemaAdvanced, validateForman, validateFormanWithDomains };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -513,11 +513,13 @@ var SchemaConversionError = class extends Error {
|
|
|
513
513
|
constructor(message, field) {
|
|
514
514
|
super(message);
|
|
515
515
|
this.name = "SchemaConversionError";
|
|
516
|
+
this.field = field;
|
|
516
517
|
}
|
|
517
518
|
};
|
|
518
519
|
var FORMAN_TYPE_MAP = {
|
|
519
520
|
account: "number",
|
|
520
521
|
hook: "number",
|
|
522
|
+
device: "number",
|
|
521
523
|
keychain: "number",
|
|
522
524
|
datastore: "number",
|
|
523
525
|
aiagent: "string",
|
|
@@ -561,14 +563,29 @@ var FORMAN_TYPE_MAP = {
|
|
|
561
563
|
uuid: "string",
|
|
562
564
|
any: void 0
|
|
563
565
|
};
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
566
|
+
var FORMAN_TYPE_LOOKUP = new Map(
|
|
567
|
+
Object.keys(FORMAN_TYPE_MAP).map((key) => [key.toLowerCase(), key])
|
|
568
|
+
);
|
|
569
|
+
var FORMAN_TYPE_ALIASES = {
|
|
570
|
+
// Forman's plain-string primitive is `text`; there is no `string` type.
|
|
571
|
+
string: "text",
|
|
572
|
+
bool: "boolean",
|
|
573
|
+
// Forman's `date` already carries time (see `time` on FormanSchemaField, default true).
|
|
574
|
+
datetime: "date",
|
|
575
|
+
// JSON Schema has no float; `number` is the only target.
|
|
576
|
+
float: "number",
|
|
577
|
+
// `upload` is the validator's existing name for the same array-shaped type.
|
|
578
|
+
upload: "filestorage"
|
|
579
|
+
};
|
|
580
|
+
function resolveFormanFieldType(rawType) {
|
|
581
|
+
if (!rawType) return void 0;
|
|
582
|
+
const base = rawType.includes(":") ? rawType.split(":")[0] : rawType;
|
|
583
|
+
const lower = base.toLowerCase();
|
|
584
|
+
return FORMAN_TYPE_LOOKUP.get(lower) ?? FORMAN_TYPE_ALIASES[lower];
|
|
585
|
+
}
|
|
586
|
+
function canonicalTypeWithKind(rawType, canonicalType) {
|
|
587
|
+
const separatorIndex = rawType.indexOf(":");
|
|
588
|
+
return separatorIndex === -1 ? canonicalType : `${canonicalType}${rawType.slice(separatorIndex)}`;
|
|
572
589
|
}
|
|
573
590
|
function appendQueryString(path, domain, tail) {
|
|
574
591
|
if (path.startsWith("api://")) return path;
|
|
@@ -586,6 +603,7 @@ function createDefaultContext(options) {
|
|
|
586
603
|
roots: {},
|
|
587
604
|
definitions: {},
|
|
588
605
|
excludeAdvancedFields: options?.excludeAdvancedFields ?? false,
|
|
606
|
+
strictFieldTypes: options?.strictFieldTypes ?? false,
|
|
589
607
|
skippedPaths: {},
|
|
590
608
|
addConditionalFields: () => {
|
|
591
609
|
throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
|
|
@@ -597,8 +615,19 @@ var compositeHandlers = {
|
|
|
597
615
|
udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
|
|
598
616
|
};
|
|
599
617
|
function toJSONSchemaInternal(field, context) {
|
|
600
|
-
|
|
601
|
-
|
|
618
|
+
const canonicalType = resolveFormanFieldType(field.type);
|
|
619
|
+
if (!canonicalType) {
|
|
620
|
+
if (context.strictFieldTypes) {
|
|
621
|
+
throw new SchemaConversionError(
|
|
622
|
+
field.type ? `Unknown field type: ${field.type}` : "Field type is required",
|
|
623
|
+
field
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
return degradeUnconvertibleField(field, context);
|
|
627
|
+
}
|
|
628
|
+
const normalizedField = normalizeFormanFieldType(
|
|
629
|
+
canonicalType === field.type ? field : { ...field, type: canonicalTypeWithKind(field.type, canonicalType) }
|
|
630
|
+
);
|
|
602
631
|
const handler = compositeHandlers[normalizedField.type];
|
|
603
632
|
if (handler) {
|
|
604
633
|
const type = normalizedField.type;
|
|
@@ -1013,6 +1042,16 @@ function handleJsonType(field, result) {
|
|
|
1013
1042
|
});
|
|
1014
1043
|
return result;
|
|
1015
1044
|
}
|
|
1045
|
+
function degradeUnconvertibleField(field, context) {
|
|
1046
|
+
const path = field.name ? [...context.path, field.name].join(".") : context.path.join(".");
|
|
1047
|
+
const reason = field.type ? `unknown type: ${field.type}` : "missing type";
|
|
1048
|
+
(context.skippedPaths.unconvertible ||= []).push(`${path} (${reason})`);
|
|
1049
|
+
return {
|
|
1050
|
+
type: FORMAN_TYPE_MAP["any"],
|
|
1051
|
+
title: noEmpty(field.label),
|
|
1052
|
+
description: noEmpty(field.help)
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1016
1055
|
function handlePrimitiveType(field, result, context) {
|
|
1017
1056
|
if (field.default !== "" && field.default != null) {
|
|
1018
1057
|
result.default = field.default;
|
|
@@ -1329,6 +1368,19 @@ async function validateFormanValue(value, field, context) {
|
|
|
1329
1368
|
warnings: []
|
|
1330
1369
|
};
|
|
1331
1370
|
}
|
|
1371
|
+
if (!field.type) {
|
|
1372
|
+
return {
|
|
1373
|
+
valid: false,
|
|
1374
|
+
errors: [
|
|
1375
|
+
{
|
|
1376
|
+
domain: context.domain,
|
|
1377
|
+
path: context.path.join("."),
|
|
1378
|
+
message: "Field type is required."
|
|
1379
|
+
}
|
|
1380
|
+
],
|
|
1381
|
+
warnings: []
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1332
1384
|
const normalizedField = normalizeFormanFieldType(field);
|
|
1333
1385
|
if (normalizedField.required && (value == null || value === "")) {
|
|
1334
1386
|
return {
|
|
@@ -2274,6 +2326,9 @@ function toJSONSchemaAdvanced(field, options) {
|
|
|
2274
2326
|
if (context.skippedPaths.advanced?.length) {
|
|
2275
2327
|
skippedPaths.advanced = context.skippedPaths.advanced;
|
|
2276
2328
|
}
|
|
2329
|
+
if (context.skippedPaths.unconvertible?.length) {
|
|
2330
|
+
skippedPaths.unconvertible = context.skippedPaths.unconvertible;
|
|
2331
|
+
}
|
|
2277
2332
|
return {
|
|
2278
2333
|
schema,
|
|
2279
2334
|
...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
|
|
@@ -2292,6 +2347,8 @@ function validateForman(values, schema, options, restoreExtras) {
|
|
|
2292
2347
|
);
|
|
2293
2348
|
}
|
|
2294
2349
|
export {
|
|
2350
|
+
SchemaConversionError,
|
|
2351
|
+
resolveFormanFieldType,
|
|
2295
2352
|
toFormanSchema,
|
|
2296
2353
|
toJSONSchema,
|
|
2297
2354
|
toJSONSchemaAdvanced,
|