@makehq/forman-schema 1.15.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 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. It includes a message and optionally the field that caused the error.
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,
@@ -216,7 +218,7 @@ function buildRestoreStructure(items) {
216
218
  }
217
219
  current = current[key].items;
218
220
  } else {
219
- if (!current[key].nested) {
221
+ if (!current[key].nested || typeof current[key].nested !== "object" || Array.isArray(current[key].nested)) {
220
222
  current[key].nested = {};
221
223
  }
222
224
  current = current[key].nested;
@@ -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
- function validateFormanField(field) {
595
- if (!field.type) {
596
- throw new SchemaConversionError("Field type is required", field);
597
- }
598
- const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
599
- if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
600
- throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
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
- validateFormanField(field);
631
- const normalizedField = normalizeFormanFieldType(field);
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 {
@@ -1917,7 +1971,8 @@ async function handleSelectType(value, field, context) {
1917
1971
  path: context.path,
1918
1972
  state: {
1919
1973
  mode: "chose",
1920
- label: placeholder.label
1974
+ label: placeholder.label,
1975
+ ...placeholder.nested ? { nested: placeholder.nested } : {}
1921
1976
  }
1922
1977
  });
1923
1978
  }
@@ -1955,7 +2010,8 @@ async function handleSelectType(value, field, context) {
1955
2010
  path: context.path,
1956
2011
  state: {
1957
2012
  mode: isReferenceType(field.type) ? void 0 : "chose",
1958
- label: item.label
2013
+ label: item.label,
2014
+ ...item.nested ? { nested: item.nested } : {}
1959
2015
  }
1960
2016
  });
1961
2017
  }
@@ -2302,6 +2358,9 @@ function toJSONSchemaAdvanced(field, options) {
2302
2358
  if (context.skippedPaths.advanced?.length) {
2303
2359
  skippedPaths.advanced = context.skippedPaths.advanced;
2304
2360
  }
2361
+ if (context.skippedPaths.unconvertible?.length) {
2362
+ skippedPaths.unconvertible = context.skippedPaths.unconvertible;
2363
+ }
2305
2364
  return {
2306
2365
  schema,
2307
2366
  ...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
@@ -2321,6 +2380,8 @@ function validateForman(values, schema, options, restoreExtras) {
2321
2380
  }
2322
2381
  // Annotate the CommonJS export names for ESM import in node:
2323
2382
  0 && (module.exports = {
2383
+ SchemaConversionError,
2384
+ resolveFormanFieldType,
2324
2385
  toFormanSchema,
2325
2386
  toJSONSchema,
2326
2387
  toJSONSchemaAdvanced,
package/dist/index.d.cts CHANGED
@@ -221,7 +221,12 @@ type FormanSchemaFieldState = {
221
221
  path?: Array<string>;
222
222
  data?: Record<string, unknown>;
223
223
  extra?: Record<string, unknown>;
224
- nested?: Record<string, FormanSchemaFieldState>;
224
+ /**
225
+ * Child field states (record, built from field paths) or, on `chose` states of
226
+ * select-like fields, the chosen option's nested field specification — the UI
227
+ * persists that spec in `metadata.restore` to render the dependent fields.
228
+ */
229
+ nested?: Record<string, FormanSchemaFieldState> | FormanSchemaNested;
225
230
  items?: Record<string, FormanSchemaFieldState>[];
226
231
  };
227
232
  /**
@@ -235,6 +240,14 @@ type FormanJsonSchemaOptions = {
235
240
  * `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
236
241
  */
237
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;
238
251
  };
239
252
  /**
240
253
  * Result of converting a Forman Schema to JSON Schema
@@ -246,6 +259,12 @@ type FormanJsonSchemaResult = {
246
259
  skippedPaths?: {
247
260
  /** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
248
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[];
249
268
  };
250
269
  };
251
270
  /**
@@ -289,6 +308,26 @@ type FormanValidationOptions = {
289
308
  */
290
309
  declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
291
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
+
292
331
  /**
293
332
  * Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
294
333
  * fields that were skipped during conversion.
@@ -306,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
306
345
  * `definitions[type]`; advanced fields inside a composite template are recorded with the
307
346
  * path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
308
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
+ *
309
355
  * If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
310
356
  *
311
357
  * @param field The Forman Schema field to convert
@@ -321,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
321
367
  * fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
322
368
  * which returns `{ schema, skippedPaths? }`.
323
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
+ *
324
373
  * @param field The Forman Schema field to convert
325
374
  * @param options Conversion options
326
375
  * @returns The equivalent JSON Schema
@@ -353,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
353
402
  */
354
403
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
355
404
 
356
- 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
@@ -221,7 +221,12 @@ type FormanSchemaFieldState = {
221
221
  path?: Array<string>;
222
222
  data?: Record<string, unknown>;
223
223
  extra?: Record<string, unknown>;
224
- nested?: Record<string, FormanSchemaFieldState>;
224
+ /**
225
+ * Child field states (record, built from field paths) or, on `chose` states of
226
+ * select-like fields, the chosen option's nested field specification — the UI
227
+ * persists that spec in `metadata.restore` to render the dependent fields.
228
+ */
229
+ nested?: Record<string, FormanSchemaFieldState> | FormanSchemaNested;
225
230
  items?: Record<string, FormanSchemaFieldState>[];
226
231
  };
227
232
  /**
@@ -235,6 +240,14 @@ type FormanJsonSchemaOptions = {
235
240
  * `toJSONSchemaAdvanced`'s `skippedPaths.advanced` so the caller can re-request them.
236
241
  */
237
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;
238
251
  };
239
252
  /**
240
253
  * Result of converting a Forman Schema to JSON Schema
@@ -246,6 +259,12 @@ type FormanJsonSchemaResult = {
246
259
  skippedPaths?: {
247
260
  /** Dot-notation paths of advanced fields that were skipped. Present only when at least one advanced field was skipped. */
248
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[];
249
268
  };
250
269
  };
251
270
  /**
@@ -289,6 +308,26 @@ type FormanValidationOptions = {
289
308
  */
290
309
  declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
291
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
+
292
331
  /**
293
332
  * Converts a Forman Schema field to its JSON Schema equivalent and reports the paths of any
294
333
  * fields that were skipped during conversion.
@@ -306,6 +345,13 @@ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
306
345
  * `definitions[type]`; advanced fields inside a composite template are recorded with the
307
346
  * path of the FIRST usage only. See the comment near `compositeHandlers` in `src/forman.ts`.
308
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
+ *
309
355
  * If you don't need `skippedPaths`, use {@link toJSONSchema} which returns just the schema.
310
356
  *
311
357
  * @param field The Forman Schema field to convert
@@ -321,6 +367,9 @@ declare function toJSONSchemaAdvanced(field: FormanSchemaField, options?: Forman
321
367
  * fields were dropped (e.g. to render a "show advanced" toggle), use {@link toJSONSchemaAdvanced}
322
368
  * which returns `{ schema, skippedPaths? }`.
323
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
+ *
324
373
  * @param field The Forman Schema field to convert
325
374
  * @param options Conversion options
326
375
  * @returns The equivalent JSON Schema
@@ -353,4 +402,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
353
402
  */
354
403
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
355
404
 
356
- 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
@@ -186,7 +186,7 @@ function buildRestoreStructure(items) {
186
186
  }
187
187
  current = current[key].items;
188
188
  } else {
189
- if (!current[key].nested) {
189
+ if (!current[key].nested || typeof current[key].nested !== "object" || Array.isArray(current[key].nested)) {
190
190
  current[key].nested = {};
191
191
  }
192
192
  current = current[key].nested;
@@ -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
- function validateFormanField(field) {
565
- if (!field.type) {
566
- throw new SchemaConversionError("Field type is required", field);
567
- }
568
- const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
569
- if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
570
- throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
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
- validateFormanField(field);
601
- const normalizedField = normalizeFormanFieldType(field);
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 {
@@ -1887,7 +1939,8 @@ async function handleSelectType(value, field, context) {
1887
1939
  path: context.path,
1888
1940
  state: {
1889
1941
  mode: "chose",
1890
- label: placeholder.label
1942
+ label: placeholder.label,
1943
+ ...placeholder.nested ? { nested: placeholder.nested } : {}
1891
1944
  }
1892
1945
  });
1893
1946
  }
@@ -1925,7 +1978,8 @@ async function handleSelectType(value, field, context) {
1925
1978
  path: context.path,
1926
1979
  state: {
1927
1980
  mode: isReferenceType(field.type) ? void 0 : "chose",
1928
- label: item.label
1981
+ label: item.label,
1982
+ ...item.nested ? { nested: item.nested } : {}
1929
1983
  }
1930
1984
  });
1931
1985
  }
@@ -2272,6 +2326,9 @@ function toJSONSchemaAdvanced(field, options) {
2272
2326
  if (context.skippedPaths.advanced?.length) {
2273
2327
  skippedPaths.advanced = context.skippedPaths.advanced;
2274
2328
  }
2329
+ if (context.skippedPaths.unconvertible?.length) {
2330
+ skippedPaths.unconvertible = context.skippedPaths.unconvertible;
2331
+ }
2275
2332
  return {
2276
2333
  schema,
2277
2334
  ...Object.keys(skippedPaths).length > 0 ? { skippedPaths } : {}
@@ -2290,6 +2347,8 @@ function validateForman(values, schema, options, restoreExtras) {
2290
2347
  );
2291
2348
  }
2292
2349
  export {
2350
+ SchemaConversionError,
2351
+ resolveFormanFieldType,
2293
2352
  toFormanSchema,
2294
2353
  toJSONSchema,
2295
2354
  toJSONSchemaAdvanced,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",