@makehq/forman-schema 1.14.0 → 1.15.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
@@ -80,6 +80,56 @@ const jsonSchemaField = {
80
80
  const formanSchema = toFormanSchema(jsonSchemaField);
81
81
  ```
82
82
 
83
+ ### JSON fields (`type: 'json'`)
84
+
85
+ A `json` field can carry an explicit `schema` (a JSON Schema). This lets you author complex parts of a form directly in JSON Schema and mix them with primitive Forman fields:
86
+
87
+ ```typescript
88
+ const formanField = {
89
+ type: 'collection',
90
+ spec: [
91
+ { name: 'title', type: 'text' },
92
+ {
93
+ name: 'input',
94
+ type: 'json',
95
+ schema: {
96
+ type: 'object',
97
+ properties: {
98
+ name: { type: 'string' },
99
+ age: { type: 'number' },
100
+ },
101
+ },
102
+ },
103
+ ],
104
+ };
105
+ ```
106
+
107
+ On conversion, the `schema` is **echoed verbatim** into the JSON Schema output (the field's `label`/`help` fill in `title`/`description` only when the schema omits them). An enumerable `x-json` marker is added so `toFormanSchema` can recover the `json` type; it survives JSON serialization. A `json` field **without** a `schema` renders as a plain object schema (`{ type: 'object' }`), since a JSON value is most naturally an object.
108
+
109
+ #### External validators
110
+
111
+ The library cannot validate a JSON value against an arbitrary JSON Schema on its own — it has **no JSON Schema validator built in**. Validation of `json` fields is therefore opt-in: **a `json` value is not validated unless you provide a `validateJson` callback.** Without it, the value passes through untouched.
112
+
113
+ This is the first of a general **external validator** concept: a callback that performs validation the library can't, and returns a `FormanExternalValidationResult` verdict (`{ valid, errors?, warnings? }`) that is spliced into the overall result. The callback may be async (awaited), and its `errors`/`warnings` are stamped with the field's domain and path automatically. A `valid: false` verdict always fails validation, even when it carries no messages.
114
+
115
+ ```typescript
116
+ import { validateForman, type FormanExternalValidationResult } from '@makehq/forman-schema';
117
+ import Ajv from 'ajv'; // any JSON Schema validator works
118
+
119
+ const ajv = new Ajv({ allErrors: true });
120
+
121
+ const result = await validateForman({ input: { name: 'Alice', age: 30 } }, schema, {
122
+ async validateJson(schema, value): Promise<FormanExternalValidationResult> {
123
+ const validate = ajv.compile(schema);
124
+ if (validate(value)) return { valid: true };
125
+ return {
126
+ valid: false,
127
+ errors: (validate.errors ?? []).map(e => `${e.instancePath} ${e.message}`),
128
+ };
129
+ },
130
+ });
131
+ ```
132
+
83
133
  ### Validation
84
134
 
85
135
  Validate Forman values against a Forman Schema. Two entry points are available:
@@ -217,7 +267,7 @@ const result = await validateFormanWithDomains(
217
267
  - hidden → string
218
268
  - hook → number
219
269
  - integer → number
220
- - json → string
270
+ - json → object (or its `schema` echoed verbatim when provided — see [JSON fields](#json-fields-type-json))
221
271
  - keychain → number
222
272
  - number → number
223
273
  - path → string
package/dist/index.cjs CHANGED
@@ -561,7 +561,7 @@ var FORMAN_TYPE_MAP = {
561
561
  boolean: "boolean",
562
562
  checkbox: "boolean",
563
563
  date: "string",
564
- json: "string",
564
+ json: "object",
565
565
  buffer: "string",
566
566
  cert: "string",
567
567
  color: "string",
@@ -671,6 +671,8 @@ function toJSONSchemaInternal(field, context) {
671
671
  case "collection":
672
672
  case "dynamicCollection":
673
673
  return handleCollectionType(normalizedField, result, context);
674
+ case "json":
675
+ return handleJsonType(normalizedField, result);
674
676
  case "array":
675
677
  case "filestorage":
676
678
  return handleArrayType(normalizedField, result, context);
@@ -1027,6 +1029,20 @@ function handleSelectOrPathType(field, result, context) {
1027
1029
  if (field.rpc) result = processRpcDirective(field, result, context);
1028
1030
  return result;
1029
1031
  }
1032
+ function handleJsonType(field, result) {
1033
+ if (!isObject(field.schema)) {
1034
+ return result;
1035
+ }
1036
+ delete result.type;
1037
+ Object.assign(result, field.schema);
1038
+ Object.defineProperty(result, "x-json", {
1039
+ configurable: true,
1040
+ enumerable: true,
1041
+ writable: true,
1042
+ value: true
1043
+ });
1044
+ return result;
1045
+ }
1030
1046
  function handlePrimitiveType(field, result, context) {
1031
1047
  if (field.default !== "" && field.default != null) {
1032
1048
  result.default = field.default;
@@ -1146,7 +1162,8 @@ var SCHEMA_STRIP_KEYS = [
1146
1162
  "tags",
1147
1163
  "extension",
1148
1164
  "codepage",
1149
- "logic"
1165
+ "logic",
1166
+ "schema"
1150
1167
  ];
1151
1168
  function clampFieldForSchema(field) {
1152
1169
  const clamped = { ...field };
@@ -1191,7 +1208,7 @@ var FORMAN_TYPE_MAP2 = {
1191
1208
  boolean: "boolean",
1192
1209
  checkbox: "boolean",
1193
1210
  date: "string",
1194
- json: "string",
1211
+ json: void 0,
1195
1212
  buffer: "string",
1196
1213
  cert: "string",
1197
1214
  color: "string",
@@ -1286,7 +1303,8 @@ async function validateFormanWithDomainsInternal(domains, options) {
1286
1303
  ...data,
1287
1304
  ...localData
1288
1305
  });
1289
- }
1306
+ },
1307
+ validateJson: options?.validateJson
1290
1308
  }
1291
1309
  );
1292
1310
  errors.push(...result.errors);
@@ -1418,6 +1436,8 @@ async function validateFormanValue(value, field, context) {
1418
1436
  return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1419
1437
  }
1420
1438
  switch (normalizedField.type) {
1439
+ case "json":
1440
+ return handleJsonType2(value, normalizedField, context);
1421
1441
  case "collection":
1422
1442
  return handleCollectionType2(value, normalizedField, context);
1423
1443
  case "array":
@@ -2018,6 +2038,35 @@ async function handleNestedFields(nested, value, field, context) {
2018
2038
  warnings
2019
2039
  };
2020
2040
  }
2041
+ async function handleJsonType2(value, field, context) {
2042
+ const errors = [];
2043
+ const warnings = [];
2044
+ if (!isObject(field.schema) || !context.validateJson) {
2045
+ return { valid: true, errors, warnings };
2046
+ }
2047
+ const path = context.path.join(".");
2048
+ let fragment;
2049
+ try {
2050
+ fragment = await context.validateJson(field.schema, value);
2051
+ } catch (err) {
2052
+ const message = err instanceof Error ? err.message : "JSON schema validation failed unexpectedly.";
2053
+ return { valid: false, errors: [{ domain: context.domain, path, message }], warnings };
2054
+ }
2055
+ for (const message of fragment.errors ?? []) {
2056
+ errors.push({ domain: context.domain, path, message });
2057
+ }
2058
+ for (const message of fragment.warnings ?? []) {
2059
+ warnings.push({ domain: context.domain, path, message });
2060
+ }
2061
+ if (fragment.valid === false && errors.length === 0) {
2062
+ errors.push({ domain: context.domain, path, message: "JSON value failed schema validation." });
2063
+ }
2064
+ return {
2065
+ valid: errors.length === 0,
2066
+ errors,
2067
+ warnings
2068
+ };
2069
+ }
2021
2070
  async function handlePrimitiveType2(value, field, context) {
2022
2071
  const errors = [];
2023
2072
  const warnings = [];
@@ -2098,6 +2147,11 @@ function toFormanSchema(field) {
2098
2147
  return result;
2099
2148
  }
2100
2149
  function toFormanSchemaInternal(field) {
2150
+ if (Object.getOwnPropertyDescriptor(field, "x-json")?.value === true) {
2151
+ const schema = { ...field };
2152
+ delete schema["x-json"];
2153
+ return { type: "json", schema };
2154
+ }
2101
2155
  const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
2102
2156
  if (compositeType === "udttype") return udttypeCollapse(field);
2103
2157
  if (compositeType === "udtspec") return udtspecCollapse(field);
package/dist/index.d.cts CHANGED
@@ -41,6 +41,8 @@ type FormanSchemaField = {
41
41
  help?: string;
42
42
  /** Sub-fields specification for collection or array types */
43
43
  spec?: FormanSchemaField[] | FormanSchemaField;
44
+ /** JSON Schema for `json` typed fields */
45
+ schema?: JSONSchema7;
44
46
  /** Hide field behind advanced toggle */
45
47
  advanced?: boolean;
46
48
  /** Human readable label for the field */
@@ -246,6 +248,19 @@ type FormanJsonSchemaResult = {
246
248
  advanced?: string[];
247
249
  };
248
250
  };
251
+ /**
252
+ * Verdict fragment returned by an external validation callback (e.g. `validateJson`) that the
253
+ * library cannot perform itself. Spliced into the overall validation result: `errors`/`warnings`
254
+ * are stamped with the field's domain and path.
255
+ */
256
+ type FormanExternalValidationResult = {
257
+ /** Whether the value is valid. A `false` verdict always fails validation, even with no messages. */
258
+ valid: boolean;
259
+ /** Error messages (cause validation to fail) */
260
+ errors?: string[];
261
+ /** Warning messages (do not affect validity) */
262
+ warnings?: string[];
263
+ };
249
264
  type FormanValidationOptions = {
250
265
  /** Unknown fields are not allowed when strict is true */
251
266
  strict?: boolean;
@@ -255,6 +270,10 @@ type FormanValidationOptions = {
255
270
  schemas?: boolean;
256
271
  /** Remote resource resolver */
257
272
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
273
+ /** Validator for `json` typed fields. Receives the field's JSON Schema and the value, and
274
+ * returns (or resolves to) a result fragment that is spliced into the overall validation
275
+ * result. When omitted, `json` fields with a `schema` pass without schema validation. */
276
+ validateJson?(schema: JSONSchema7, value: unknown): FormanExternalValidationResult | Promise<FormanExternalValidationResult>;
258
277
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
259
278
  domainAliases?: Record<string, string>;
260
279
  /** Whether to allow dynamic values (IML expressions, unresolved RPC options).
@@ -334,4 +353,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
334
353
  */
335
354
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
336
355
 
337
- export { 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ type FormanSchemaField = {
41
41
  help?: string;
42
42
  /** Sub-fields specification for collection or array types */
43
43
  spec?: FormanSchemaField[] | FormanSchemaField;
44
+ /** JSON Schema for `json` typed fields */
45
+ schema?: JSONSchema7;
44
46
  /** Hide field behind advanced toggle */
45
47
  advanced?: boolean;
46
48
  /** Human readable label for the field */
@@ -246,6 +248,19 @@ type FormanJsonSchemaResult = {
246
248
  advanced?: string[];
247
249
  };
248
250
  };
251
+ /**
252
+ * Verdict fragment returned by an external validation callback (e.g. `validateJson`) that the
253
+ * library cannot perform itself. Spliced into the overall validation result: `errors`/`warnings`
254
+ * are stamped with the field's domain and path.
255
+ */
256
+ type FormanExternalValidationResult = {
257
+ /** Whether the value is valid. A `false` verdict always fails validation, even with no messages. */
258
+ valid: boolean;
259
+ /** Error messages (cause validation to fail) */
260
+ errors?: string[];
261
+ /** Warning messages (do not affect validity) */
262
+ warnings?: string[];
263
+ };
249
264
  type FormanValidationOptions = {
250
265
  /** Unknown fields are not allowed when strict is true */
251
266
  strict?: boolean;
@@ -255,6 +270,10 @@ type FormanValidationOptions = {
255
270
  schemas?: boolean;
256
271
  /** Remote resource resolver */
257
272
  resolveRemote?(path: string, data: Record<string, unknown>): Promise<unknown>;
273
+ /** Validator for `json` typed fields. Receives the field's JSON Schema and the value, and
274
+ * returns (or resolves to) a result fragment that is spliced into the overall validation
275
+ * result. When omitted, `json` fields with a `schema` pass without schema validation. */
276
+ validateJson?(schema: JSONSchema7, value: unknown): FormanExternalValidationResult | Promise<FormanExternalValidationResult>;
258
277
  /** Maps domain names used in nested.domain to actual domain keys passed to validateFormanWithDomains */
259
278
  domainAliases?: Record<string, string>;
260
279
  /** Whether to allow dynamic values (IML expressions, unresolved RPC options).
@@ -334,4 +353,4 @@ declare function validateFormanWithDomains(domains: Record<string, {
334
353
  */
335
354
  declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
336
355
 
337
- export { 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 };
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 };
package/dist/index.js CHANGED
@@ -531,7 +531,7 @@ var FORMAN_TYPE_MAP = {
531
531
  boolean: "boolean",
532
532
  checkbox: "boolean",
533
533
  date: "string",
534
- json: "string",
534
+ json: "object",
535
535
  buffer: "string",
536
536
  cert: "string",
537
537
  color: "string",
@@ -641,6 +641,8 @@ function toJSONSchemaInternal(field, context) {
641
641
  case "collection":
642
642
  case "dynamicCollection":
643
643
  return handleCollectionType(normalizedField, result, context);
644
+ case "json":
645
+ return handleJsonType(normalizedField, result);
644
646
  case "array":
645
647
  case "filestorage":
646
648
  return handleArrayType(normalizedField, result, context);
@@ -997,6 +999,20 @@ function handleSelectOrPathType(field, result, context) {
997
999
  if (field.rpc) result = processRpcDirective(field, result, context);
998
1000
  return result;
999
1001
  }
1002
+ function handleJsonType(field, result) {
1003
+ if (!isObject(field.schema)) {
1004
+ return result;
1005
+ }
1006
+ delete result.type;
1007
+ Object.assign(result, field.schema);
1008
+ Object.defineProperty(result, "x-json", {
1009
+ configurable: true,
1010
+ enumerable: true,
1011
+ writable: true,
1012
+ value: true
1013
+ });
1014
+ return result;
1015
+ }
1000
1016
  function handlePrimitiveType(field, result, context) {
1001
1017
  if (field.default !== "" && field.default != null) {
1002
1018
  result.default = field.default;
@@ -1116,7 +1132,8 @@ var SCHEMA_STRIP_KEYS = [
1116
1132
  "tags",
1117
1133
  "extension",
1118
1134
  "codepage",
1119
- "logic"
1135
+ "logic",
1136
+ "schema"
1120
1137
  ];
1121
1138
  function clampFieldForSchema(field) {
1122
1139
  const clamped = { ...field };
@@ -1161,7 +1178,7 @@ var FORMAN_TYPE_MAP2 = {
1161
1178
  boolean: "boolean",
1162
1179
  checkbox: "boolean",
1163
1180
  date: "string",
1164
- json: "string",
1181
+ json: void 0,
1165
1182
  buffer: "string",
1166
1183
  cert: "string",
1167
1184
  color: "string",
@@ -1256,7 +1273,8 @@ async function validateFormanWithDomainsInternal(domains, options) {
1256
1273
  ...data,
1257
1274
  ...localData
1258
1275
  });
1259
- }
1276
+ },
1277
+ validateJson: options?.validateJson
1260
1278
  }
1261
1279
  );
1262
1280
  errors.push(...result.errors);
@@ -1388,6 +1406,8 @@ async function validateFormanValue(value, field, context) {
1388
1406
  return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1389
1407
  }
1390
1408
  switch (normalizedField.type) {
1409
+ case "json":
1410
+ return handleJsonType2(value, normalizedField, context);
1391
1411
  case "collection":
1392
1412
  return handleCollectionType2(value, normalizedField, context);
1393
1413
  case "array":
@@ -1988,6 +2008,35 @@ async function handleNestedFields(nested, value, field, context) {
1988
2008
  warnings
1989
2009
  };
1990
2010
  }
2011
+ async function handleJsonType2(value, field, context) {
2012
+ const errors = [];
2013
+ const warnings = [];
2014
+ if (!isObject(field.schema) || !context.validateJson) {
2015
+ return { valid: true, errors, warnings };
2016
+ }
2017
+ const path = context.path.join(".");
2018
+ let fragment;
2019
+ try {
2020
+ fragment = await context.validateJson(field.schema, value);
2021
+ } catch (err) {
2022
+ const message = err instanceof Error ? err.message : "JSON schema validation failed unexpectedly.";
2023
+ return { valid: false, errors: [{ domain: context.domain, path, message }], warnings };
2024
+ }
2025
+ for (const message of fragment.errors ?? []) {
2026
+ errors.push({ domain: context.domain, path, message });
2027
+ }
2028
+ for (const message of fragment.warnings ?? []) {
2029
+ warnings.push({ domain: context.domain, path, message });
2030
+ }
2031
+ if (fragment.valid === false && errors.length === 0) {
2032
+ errors.push({ domain: context.domain, path, message: "JSON value failed schema validation." });
2033
+ }
2034
+ return {
2035
+ valid: errors.length === 0,
2036
+ errors,
2037
+ warnings
2038
+ };
2039
+ }
1991
2040
  async function handlePrimitiveType2(value, field, context) {
1992
2041
  const errors = [];
1993
2042
  const warnings = [];
@@ -2068,6 +2117,11 @@ function toFormanSchema(field) {
2068
2117
  return result;
2069
2118
  }
2070
2119
  function toFormanSchemaInternal(field) {
2120
+ if (Object.getOwnPropertyDescriptor(field, "x-json")?.value === true) {
2121
+ const schema = { ...field };
2122
+ delete schema["x-json"];
2123
+ return { type: "json", schema };
2124
+ }
2071
2125
  const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
2072
2126
  if (compositeType === "udttype") return udttypeCollapse(field);
2073
2127
  if (compositeType === "udtspec") return udtspecCollapse(field);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",