@makehq/forman-schema 1.8.3 → 1.9.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/dist/index.cjs CHANGED
@@ -98,6 +98,60 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
98
98
  const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
99
99
  return found;
100
100
  }
101
+ function pathToString(path) {
102
+ let result = "";
103
+ for (const key of path) {
104
+ if (typeof key === "number") {
105
+ result += `[${key}]`;
106
+ } else {
107
+ if (result.length > 0) {
108
+ result += ".";
109
+ }
110
+ if (key.includes("`")) {
111
+ throw new Error(`Invalid path key: backticks are not allowed in key "${key}"`);
112
+ }
113
+ result += key.includes(".") ? `\`${key}\`` : key;
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+ function stringToPath(str) {
119
+ const path = [];
120
+ let i = 0;
121
+ while (i < str.length) {
122
+ if (str[i] === ".") {
123
+ i++;
124
+ if (i >= str.length) break;
125
+ }
126
+ if (str[i] === "[") {
127
+ const end = str.indexOf("]", i);
128
+ if (end === -1) {
129
+ throw new Error(`Invalid path: missing closing bracket in "${str}"`);
130
+ }
131
+ const num = Number(str.slice(i + 1, end));
132
+ if (!Number.isInteger(num) || num < 0) {
133
+ throw new Error(`Invalid path: non-numeric or negative index in "${str}"`);
134
+ }
135
+ path.push(num);
136
+ i = end + 1;
137
+ } else if (str[i] === "`") {
138
+ const end = str.indexOf("`", i + 1);
139
+ if (end === -1) {
140
+ throw new Error(`Invalid path: missing closing backtick in "${str}"`);
141
+ }
142
+ path.push(str.slice(i + 1, end));
143
+ i = end + 1;
144
+ } else {
145
+ let end = i;
146
+ while (end < str.length && str[end] !== "." && str[end] !== "[") {
147
+ end++;
148
+ }
149
+ path.push(str.slice(i, end));
150
+ i = end;
151
+ }
152
+ }
153
+ return path;
154
+ }
101
155
  function buildRestoreStructure(items) {
102
156
  const result = {};
103
157
  for (const item of items) {
@@ -223,6 +277,248 @@ var IML_BINARY_FILTER_OPERATORS = [
223
277
  ];
224
278
  var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
225
279
 
280
+ // src/composites/udttype.ts
281
+ function udttypeExpand(field) {
282
+ field.type = "select";
283
+ field.options = {
284
+ store: [
285
+ {
286
+ label: "Array",
287
+ value: "array",
288
+ nested: [
289
+ {
290
+ name: "spec",
291
+ type: "collection",
292
+ label: "Array Item Specification",
293
+ spec: [
294
+ {
295
+ name: "type",
296
+ label: "Type",
297
+ type: "udttype",
298
+ required: true,
299
+ default: "text"
300
+ }
301
+ ],
302
+ mappable: false
303
+ },
304
+ {
305
+ name: "required",
306
+ label: "Required",
307
+ type: "boolean",
308
+ required: true,
309
+ default: false,
310
+ mappable: false
311
+ }
312
+ ]
313
+ },
314
+ {
315
+ label: "Collection",
316
+ value: "collection",
317
+ nested: [
318
+ {
319
+ name: "spec",
320
+ label: "Specification",
321
+ type: "udtspec"
322
+ },
323
+ {
324
+ name: "sequence",
325
+ label: "Preserve the order of object keys",
326
+ type: "boolean",
327
+ mappable: false
328
+ },
329
+ {
330
+ name: "required",
331
+ label: "Required",
332
+ type: "boolean",
333
+ required: true,
334
+ default: false,
335
+ mappable: false
336
+ }
337
+ ]
338
+ },
339
+ {
340
+ label: "Date",
341
+ value: "date",
342
+ nested: [
343
+ {
344
+ name: "required",
345
+ label: "Required",
346
+ type: "boolean",
347
+ required: true,
348
+ default: false,
349
+ mappable: false
350
+ }
351
+ ]
352
+ },
353
+ {
354
+ label: "Text",
355
+ value: "text",
356
+ nested: [
357
+ {
358
+ name: "default",
359
+ label: "Default value",
360
+ placeholder: "Enter default value",
361
+ type: "text"
362
+ },
363
+ {
364
+ name: "required",
365
+ label: "Required",
366
+ type: "boolean",
367
+ required: true,
368
+ default: false,
369
+ mappable: false
370
+ },
371
+ {
372
+ name: "multiline",
373
+ label: "Multi-line",
374
+ type: "boolean",
375
+ required: true,
376
+ default: false,
377
+ mappable: false
378
+ }
379
+ ]
380
+ },
381
+ {
382
+ label: "Number",
383
+ value: "number",
384
+ nested: [
385
+ {
386
+ name: "default",
387
+ label: "Default value",
388
+ placeholder: "Enter default value",
389
+ type: "number"
390
+ },
391
+ {
392
+ name: "required",
393
+ label: "Required",
394
+ type: "boolean",
395
+ required: true,
396
+ default: false,
397
+ mappable: false
398
+ }
399
+ ]
400
+ },
401
+ {
402
+ label: "Boolean",
403
+ value: "boolean",
404
+ nested: [
405
+ {
406
+ name: "default",
407
+ label: "Default value",
408
+ placeholder: "Enter default value",
409
+ type: "boolean",
410
+ mappable: false
411
+ },
412
+ {
413
+ name: "required",
414
+ label: "Required",
415
+ type: "boolean",
416
+ required: true,
417
+ default: false,
418
+ mappable: false
419
+ }
420
+ ]
421
+ },
422
+ {
423
+ label: "Binary Data",
424
+ value: "buffer",
425
+ nested: [
426
+ {
427
+ name: "required",
428
+ label: "Required",
429
+ type: "boolean",
430
+ required: true,
431
+ default: false,
432
+ mappable: false
433
+ },
434
+ {
435
+ name: "codepage",
436
+ label: "Codepage",
437
+ type: "text",
438
+ help: "Possible values: `binary`, `utf8`. Leave empty if you're not sure."
439
+ }
440
+ ]
441
+ }
442
+ ]
443
+ };
444
+ return field;
445
+ }
446
+ function udttypeExtractInner(schema) {
447
+ const { title, description, ...inner } = schema;
448
+ return inner;
449
+ }
450
+ function udttypeWrapRef(ref, field) {
451
+ return {
452
+ allOf: [{ $ref: ref }],
453
+ title: noEmpty(field.label),
454
+ description: noEmpty(field.help),
455
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
456
+ };
457
+ }
458
+ function udttypeCollapse(field) {
459
+ return {
460
+ type: "udttype",
461
+ label: noEmpty(field.title),
462
+ help: noEmpty(field.description),
463
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
464
+ };
465
+ }
466
+
467
+ // src/composites/udtspec.ts
468
+ function udtspecExpand(field) {
469
+ field.type = "array";
470
+ field.spec = [
471
+ {
472
+ name: "name",
473
+ label: "Name",
474
+ placeholder: "Enter name",
475
+ type: "text",
476
+ required: true
477
+ },
478
+ {
479
+ name: "label",
480
+ label: "Label",
481
+ help: "Display name for better readability.",
482
+ type: "text",
483
+ advanced: true
484
+ },
485
+ {
486
+ name: "help",
487
+ label: "Description",
488
+ type: "text",
489
+ multiline: true,
490
+ required: false,
491
+ placeholder: "Enter description"
492
+ },
493
+ {
494
+ name: "type",
495
+ label: "Type",
496
+ type: "udttype",
497
+ required: true,
498
+ default: "text"
499
+ }
500
+ ];
501
+ return field;
502
+ }
503
+ function udtspecExtractInner(schema) {
504
+ return schema.items;
505
+ }
506
+ function udtspecWrapRef(ref, field) {
507
+ return {
508
+ type: "array",
509
+ title: noEmpty(field.label),
510
+ description: noEmpty(field.help),
511
+ items: { $ref: ref }
512
+ };
513
+ }
514
+ function udtspecCollapse(field) {
515
+ return {
516
+ type: "udtspec",
517
+ label: noEmpty(field.title),
518
+ help: noEmpty(field.description)
519
+ };
520
+ }
521
+
226
522
  // src/forman.ts
227
523
  var SchemaConversionError = class extends Error {
228
524
  /** Field that caused the error */
@@ -269,6 +565,8 @@ var FORMAN_TYPE_MAP = {
269
565
  pkey: "string",
270
566
  port: "number",
271
567
  select: "string",
568
+ udttype: "string",
569
+ udtspec: "array",
272
570
  time: "string",
273
571
  timestamp: "string",
274
572
  timezone: "string",
@@ -298,14 +596,52 @@ function createDefaultContext() {
298
596
  tail: [],
299
597
  path: [],
300
598
  roots: {},
599
+ definitions: {},
301
600
  addConditionalFields: () => {
302
601
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
303
602
  }
304
603
  };
305
604
  }
306
- function toJSONSchemaInternal(field, context = createDefaultContext()) {
605
+ var compositeHandlers = {
606
+ udtspec: { expand: udtspecExpand, extractInner: udtspecExtractInner, wrapRef: udtspecWrapRef },
607
+ udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
608
+ };
609
+ function toJSONSchemaInternal(field, context) {
307
610
  validateFormanField(field);
308
611
  const normalizedField = normalizeFormanFieldType(field);
612
+ const handler = compositeHandlers[normalizedField.type];
613
+ if (handler) {
614
+ const type = normalizedField.type;
615
+ const ref = `#/definitions/${type}`;
616
+ if (!context.definitions?.[type]) {
617
+ if (context.definitions) {
618
+ context.definitions[type] = {};
619
+ }
620
+ const expandField = { ...normalizedField };
621
+ delete expandField.label;
622
+ delete expandField.help;
623
+ const expanded = handler.expand(expandField);
624
+ const expandedResult = toJSONSchemaInternal(expanded, context);
625
+ const inner = handler.extractInner(expandedResult);
626
+ Object.defineProperty(inner, "x-composite", {
627
+ configurable: true,
628
+ enumerable: true,
629
+ writable: true,
630
+ value: type
631
+ });
632
+ if (context.definitions) {
633
+ context.definitions[type] = inner;
634
+ }
635
+ }
636
+ const wrapper = handler.wrapRef(ref, normalizedField);
637
+ Object.defineProperty(wrapper, "x-composite", {
638
+ configurable: true,
639
+ enumerable: true,
640
+ writable: true,
641
+ value: type
642
+ });
643
+ return wrapper;
644
+ }
309
645
  const result = {
310
646
  type: FORMAN_TYPE_MAP[normalizedField.type],
311
647
  title: noEmpty(normalizedField.label),
@@ -723,6 +1059,8 @@ var FORMAN_TYPE_MAP2 = {
723
1059
  pkey: "string",
724
1060
  port: "number",
725
1061
  select: void 0,
1062
+ udttype: "string",
1063
+ udtspec: "array",
726
1064
  time: "string",
727
1065
  timestamp: "string",
728
1066
  timezone: "string",
@@ -795,6 +1133,25 @@ async function validateFormanWithDomainsInternal(domains, options) {
795
1133
  }
796
1134
  );
797
1135
  errors.push(...result.errors);
1136
+ const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1137
+ if (restoreExtras) {
1138
+ const fieldStateMap = new Map(
1139
+ roots[domain].fieldStates.map(({ path, state }) => {
1140
+ return [pathToString(path), state];
1141
+ })
1142
+ );
1143
+ for (const [fieldPath, extra] of Object.entries(restoreExtras)) {
1144
+ const state = fieldStateMap.get(fieldPath);
1145
+ if (state) {
1146
+ state.extra = extra;
1147
+ } else {
1148
+ roots[domain].fieldStates.push({
1149
+ path: stringToPath(fieldPath),
1150
+ state: { extra }
1151
+ });
1152
+ }
1153
+ }
1154
+ }
798
1155
  }
799
1156
  return {
800
1157
  valid: errors.length === 0,
@@ -863,6 +1220,12 @@ async function validateFormanValue(value, field, context) {
863
1220
  errors: []
864
1221
  };
865
1222
  }
1223
+ if (normalizedField.type === "udttype") {
1224
+ return validateFormanValue(value, udttypeExpand({ ...normalizedField }), context);
1225
+ }
1226
+ if (normalizedField.type === "udtspec") {
1227
+ return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1228
+ }
866
1229
  switch (normalizedField.type) {
867
1230
  case "collection":
868
1231
  return handleCollectionType2(value, normalizedField, context);
@@ -1212,7 +1575,21 @@ async function handleSelectType(value, field, context) {
1212
1575
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
1213
1576
  if (typeof optionsOrGroups === "string") {
1214
1577
  try {
1215
- optionsOrGroups = await context.resolveRemote(optionsOrGroups, context);
1578
+ const resolved = await context.resolveRemote(optionsOrGroups, context);
1579
+ if (resolved == null || typeof resolved !== "object") {
1580
+ return {
1581
+ valid: false,
1582
+ errors: [
1583
+ ...errors,
1584
+ {
1585
+ domain: context.domain,
1586
+ path: context.path.join("."),
1587
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1588
+ }
1589
+ ]
1590
+ };
1591
+ }
1592
+ optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1216
1593
  } catch (error) {
1217
1594
  return {
1218
1595
  valid: false,
@@ -1322,7 +1699,12 @@ async function handleNestedFields(nested, value, field, context) {
1322
1699
  for (const item of store) {
1323
1700
  if (typeof item === "string") {
1324
1701
  try {
1325
- resolvedStore.push(...await context.resolveRemote(item, context));
1702
+ const resolved = await context.resolveRemote(item, context);
1703
+ if (Array.isArray(resolved)) {
1704
+ resolvedStore.push(...resolved);
1705
+ } else if (resolved && typeof resolved === "object") {
1706
+ resolvedStore.push(resolved);
1707
+ }
1326
1708
  } catch (error) {
1327
1709
  return {
1328
1710
  valid: false,
@@ -1430,6 +1812,9 @@ var JSON_PRIMITIVE_TYPE_MAP = {
1430
1812
  boolean: "boolean"
1431
1813
  };
1432
1814
  function toFormanSchema(field) {
1815
+ const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
1816
+ if (compositeType === "udttype") return udttypeCollapse(field);
1817
+ if (compositeType === "udtspec") return udtspecCollapse(field);
1433
1818
  switch (field.type) {
1434
1819
  case "object":
1435
1820
  if (!field.properties || !Object.entries(field.properties).length) {
@@ -1552,13 +1937,23 @@ function handleSearchDirective(formanField, directive) {
1552
1937
 
1553
1938
  // src/index.ts
1554
1939
  function toJSONSchema(field) {
1555
- return toJSONSchemaInternal(field);
1940
+ const context = createDefaultContext();
1941
+ const result = toJSONSchemaInternal(field, context);
1942
+ if (Object.keys(context.definitions ?? {}).length > 0) {
1943
+ Object.defineProperty(result, "definitions", {
1944
+ configurable: true,
1945
+ enumerable: true,
1946
+ writable: true,
1947
+ value: context.definitions
1948
+ });
1949
+ }
1950
+ return result;
1556
1951
  }
1557
1952
  function validateFormanWithDomains(domains, options) {
1558
1953
  return validateFormanWithDomainsInternal(domains, options);
1559
1954
  }
1560
- function validateForman(values, schema, options) {
1561
- return validateFormanWithDomains({ default: { values, schema } }, options);
1955
+ function validateForman(values, schema, options, restoreExtras) {
1956
+ return validateFormanWithDomains({ default: { values, schema, restoreExtras } }, options);
1562
1957
  }
1563
1958
  // Annotate the CommonJS export names for ESM import in node:
1564
1959
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -205,6 +205,7 @@ type FormanSchemaFieldState = {
205
205
  label?: string;
206
206
  path?: Array<string>;
207
207
  data?: Record<string, unknown>;
208
+ extra?: Record<string, unknown>;
208
209
  nested?: Record<string, FormanSchemaFieldState>;
209
210
  items?: Record<string, FormanSchemaFieldState>[];
210
211
  };
@@ -239,14 +240,19 @@ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
239
240
  declare function validateFormanWithDomains(domains: Record<string, {
240
241
  values: Record<string, unknown>;
241
242
  schema: FormanSchemaField[];
243
+ /** Extra values injected into restore states, keyed by string path (dot notation, `[index]`, backtick-escaping). */
244
+ restoreExtras?: Record<string, Record<string, unknown>>;
242
245
  }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
243
246
  /**
244
247
  * Validates a simple Forman values against a schema
245
248
  * @param values The values to validate
246
249
  * @param schema The schema to validate against
247
250
  * @param options The validation options
251
+ * @param restoreExtras Values to be injected into restore objects of particular fields.
252
+ * Keyed by string path using dot notation for nested keys, `[index]` for array indices,
253
+ * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
248
254
  * @returns The validation result
249
255
  */
250
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
256
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
251
257
 
252
258
  export { type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
package/dist/index.d.ts CHANGED
@@ -205,6 +205,7 @@ type FormanSchemaFieldState = {
205
205
  label?: string;
206
206
  path?: Array<string>;
207
207
  data?: Record<string, unknown>;
208
+ extra?: Record<string, unknown>;
208
209
  nested?: Record<string, FormanSchemaFieldState>;
209
210
  items?: Record<string, FormanSchemaFieldState>[];
210
211
  };
@@ -239,14 +240,19 @@ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
239
240
  declare function validateFormanWithDomains(domains: Record<string, {
240
241
  values: Record<string, unknown>;
241
242
  schema: FormanSchemaField[];
243
+ /** Extra values injected into restore states, keyed by string path (dot notation, `[index]`, backtick-escaping). */
244
+ restoreExtras?: Record<string, Record<string, unknown>>;
242
245
  }>, options?: FormanValidationOptions): Promise<FormanValidationResult>;
243
246
  /**
244
247
  * Validates a simple Forman values against a schema
245
248
  * @param values The values to validate
246
249
  * @param schema The schema to validate against
247
250
  * @param options The validation options
251
+ * @param restoreExtras Values to be injected into restore objects of particular fields.
252
+ * Keyed by string path using dot notation for nested keys, `[index]` for array indices,
253
+ * and backtick-escaping for keys containing dots (e.g. `"a.b[0].c"`, `` "`dotted.key`.child" ``).
248
254
  * @returns The validation result
249
255
  */
250
- declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions): Promise<FormanValidationResult>;
256
+ declare function validateForman(values: Record<string, unknown>, schema: FormanSchemaField[], options?: FormanValidationOptions, restoreExtras?: Record<string, Record<string, unknown>>): Promise<FormanValidationResult>;
251
257
 
252
258
  export { type FormanSchemaDirectoryOption, type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaOptionGroup, type FormanSchemaPathExtendedOptions, type FormanSchemaRPCButton, type FormanSchemaValue, toFormanSchema, toJSONSchema, validateForman, validateFormanWithDomains };
package/dist/index.js CHANGED
@@ -69,6 +69,60 @@ function findValueInSelectOptions(field, value, optionsAndGroups) {
69
69
  const found = optionsAndGroups.find((option) => "value" in option && option.value === value);
70
70
  return found;
71
71
  }
72
+ function pathToString(path) {
73
+ let result = "";
74
+ for (const key of path) {
75
+ if (typeof key === "number") {
76
+ result += `[${key}]`;
77
+ } else {
78
+ if (result.length > 0) {
79
+ result += ".";
80
+ }
81
+ if (key.includes("`")) {
82
+ throw new Error(`Invalid path key: backticks are not allowed in key "${key}"`);
83
+ }
84
+ result += key.includes(".") ? `\`${key}\`` : key;
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ function stringToPath(str) {
90
+ const path = [];
91
+ let i = 0;
92
+ while (i < str.length) {
93
+ if (str[i] === ".") {
94
+ i++;
95
+ if (i >= str.length) break;
96
+ }
97
+ if (str[i] === "[") {
98
+ const end = str.indexOf("]", i);
99
+ if (end === -1) {
100
+ throw new Error(`Invalid path: missing closing bracket in "${str}"`);
101
+ }
102
+ const num = Number(str.slice(i + 1, end));
103
+ if (!Number.isInteger(num) || num < 0) {
104
+ throw new Error(`Invalid path: non-numeric or negative index in "${str}"`);
105
+ }
106
+ path.push(num);
107
+ i = end + 1;
108
+ } else if (str[i] === "`") {
109
+ const end = str.indexOf("`", i + 1);
110
+ if (end === -1) {
111
+ throw new Error(`Invalid path: missing closing backtick in "${str}"`);
112
+ }
113
+ path.push(str.slice(i + 1, end));
114
+ i = end + 1;
115
+ } else {
116
+ let end = i;
117
+ while (end < str.length && str[end] !== "." && str[end] !== "[") {
118
+ end++;
119
+ }
120
+ path.push(str.slice(i, end));
121
+ i = end;
122
+ }
123
+ }
124
+ return path;
125
+ }
72
126
  function buildRestoreStructure(items) {
73
127
  const result = {};
74
128
  for (const item of items) {
@@ -194,6 +248,248 @@ var IML_BINARY_FILTER_OPERATORS = [
194
248
  ];
195
249
  var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
196
250
 
251
+ // src/composites/udttype.ts
252
+ function udttypeExpand(field) {
253
+ field.type = "select";
254
+ field.options = {
255
+ store: [
256
+ {
257
+ label: "Array",
258
+ value: "array",
259
+ nested: [
260
+ {
261
+ name: "spec",
262
+ type: "collection",
263
+ label: "Array Item Specification",
264
+ spec: [
265
+ {
266
+ name: "type",
267
+ label: "Type",
268
+ type: "udttype",
269
+ required: true,
270
+ default: "text"
271
+ }
272
+ ],
273
+ mappable: false
274
+ },
275
+ {
276
+ name: "required",
277
+ label: "Required",
278
+ type: "boolean",
279
+ required: true,
280
+ default: false,
281
+ mappable: false
282
+ }
283
+ ]
284
+ },
285
+ {
286
+ label: "Collection",
287
+ value: "collection",
288
+ nested: [
289
+ {
290
+ name: "spec",
291
+ label: "Specification",
292
+ type: "udtspec"
293
+ },
294
+ {
295
+ name: "sequence",
296
+ label: "Preserve the order of object keys",
297
+ type: "boolean",
298
+ mappable: false
299
+ },
300
+ {
301
+ name: "required",
302
+ label: "Required",
303
+ type: "boolean",
304
+ required: true,
305
+ default: false,
306
+ mappable: false
307
+ }
308
+ ]
309
+ },
310
+ {
311
+ label: "Date",
312
+ value: "date",
313
+ nested: [
314
+ {
315
+ name: "required",
316
+ label: "Required",
317
+ type: "boolean",
318
+ required: true,
319
+ default: false,
320
+ mappable: false
321
+ }
322
+ ]
323
+ },
324
+ {
325
+ label: "Text",
326
+ value: "text",
327
+ nested: [
328
+ {
329
+ name: "default",
330
+ label: "Default value",
331
+ placeholder: "Enter default value",
332
+ type: "text"
333
+ },
334
+ {
335
+ name: "required",
336
+ label: "Required",
337
+ type: "boolean",
338
+ required: true,
339
+ default: false,
340
+ mappable: false
341
+ },
342
+ {
343
+ name: "multiline",
344
+ label: "Multi-line",
345
+ type: "boolean",
346
+ required: true,
347
+ default: false,
348
+ mappable: false
349
+ }
350
+ ]
351
+ },
352
+ {
353
+ label: "Number",
354
+ value: "number",
355
+ nested: [
356
+ {
357
+ name: "default",
358
+ label: "Default value",
359
+ placeholder: "Enter default value",
360
+ type: "number"
361
+ },
362
+ {
363
+ name: "required",
364
+ label: "Required",
365
+ type: "boolean",
366
+ required: true,
367
+ default: false,
368
+ mappable: false
369
+ }
370
+ ]
371
+ },
372
+ {
373
+ label: "Boolean",
374
+ value: "boolean",
375
+ nested: [
376
+ {
377
+ name: "default",
378
+ label: "Default value",
379
+ placeholder: "Enter default value",
380
+ type: "boolean",
381
+ mappable: false
382
+ },
383
+ {
384
+ name: "required",
385
+ label: "Required",
386
+ type: "boolean",
387
+ required: true,
388
+ default: false,
389
+ mappable: false
390
+ }
391
+ ]
392
+ },
393
+ {
394
+ label: "Binary Data",
395
+ value: "buffer",
396
+ nested: [
397
+ {
398
+ name: "required",
399
+ label: "Required",
400
+ type: "boolean",
401
+ required: true,
402
+ default: false,
403
+ mappable: false
404
+ },
405
+ {
406
+ name: "codepage",
407
+ label: "Codepage",
408
+ type: "text",
409
+ help: "Possible values: `binary`, `utf8`. Leave empty if you're not sure."
410
+ }
411
+ ]
412
+ }
413
+ ]
414
+ };
415
+ return field;
416
+ }
417
+ function udttypeExtractInner(schema) {
418
+ const { title, description, ...inner } = schema;
419
+ return inner;
420
+ }
421
+ function udttypeWrapRef(ref, field) {
422
+ return {
423
+ allOf: [{ $ref: ref }],
424
+ title: noEmpty(field.label),
425
+ description: noEmpty(field.help),
426
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
427
+ };
428
+ }
429
+ function udttypeCollapse(field) {
430
+ return {
431
+ type: "udttype",
432
+ label: noEmpty(field.title),
433
+ help: noEmpty(field.description),
434
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
435
+ };
436
+ }
437
+
438
+ // src/composites/udtspec.ts
439
+ function udtspecExpand(field) {
440
+ field.type = "array";
441
+ field.spec = [
442
+ {
443
+ name: "name",
444
+ label: "Name",
445
+ placeholder: "Enter name",
446
+ type: "text",
447
+ required: true
448
+ },
449
+ {
450
+ name: "label",
451
+ label: "Label",
452
+ help: "Display name for better readability.",
453
+ type: "text",
454
+ advanced: true
455
+ },
456
+ {
457
+ name: "help",
458
+ label: "Description",
459
+ type: "text",
460
+ multiline: true,
461
+ required: false,
462
+ placeholder: "Enter description"
463
+ },
464
+ {
465
+ name: "type",
466
+ label: "Type",
467
+ type: "udttype",
468
+ required: true,
469
+ default: "text"
470
+ }
471
+ ];
472
+ return field;
473
+ }
474
+ function udtspecExtractInner(schema) {
475
+ return schema.items;
476
+ }
477
+ function udtspecWrapRef(ref, field) {
478
+ return {
479
+ type: "array",
480
+ title: noEmpty(field.label),
481
+ description: noEmpty(field.help),
482
+ items: { $ref: ref }
483
+ };
484
+ }
485
+ function udtspecCollapse(field) {
486
+ return {
487
+ type: "udtspec",
488
+ label: noEmpty(field.title),
489
+ help: noEmpty(field.description)
490
+ };
491
+ }
492
+
197
493
  // src/forman.ts
198
494
  var SchemaConversionError = class extends Error {
199
495
  /** Field that caused the error */
@@ -240,6 +536,8 @@ var FORMAN_TYPE_MAP = {
240
536
  pkey: "string",
241
537
  port: "number",
242
538
  select: "string",
539
+ udttype: "string",
540
+ udtspec: "array",
243
541
  time: "string",
244
542
  timestamp: "string",
245
543
  timezone: "string",
@@ -269,14 +567,52 @@ function createDefaultContext() {
269
567
  tail: [],
270
568
  path: [],
271
569
  roots: {},
570
+ definitions: {},
272
571
  addConditionalFields: () => {
273
572
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
274
573
  }
275
574
  };
276
575
  }
277
- function toJSONSchemaInternal(field, context = createDefaultContext()) {
576
+ var compositeHandlers = {
577
+ udtspec: { expand: udtspecExpand, extractInner: udtspecExtractInner, wrapRef: udtspecWrapRef },
578
+ udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
579
+ };
580
+ function toJSONSchemaInternal(field, context) {
278
581
  validateFormanField(field);
279
582
  const normalizedField = normalizeFormanFieldType(field);
583
+ const handler = compositeHandlers[normalizedField.type];
584
+ if (handler) {
585
+ const type = normalizedField.type;
586
+ const ref = `#/definitions/${type}`;
587
+ if (!context.definitions?.[type]) {
588
+ if (context.definitions) {
589
+ context.definitions[type] = {};
590
+ }
591
+ const expandField = { ...normalizedField };
592
+ delete expandField.label;
593
+ delete expandField.help;
594
+ const expanded = handler.expand(expandField);
595
+ const expandedResult = toJSONSchemaInternal(expanded, context);
596
+ const inner = handler.extractInner(expandedResult);
597
+ Object.defineProperty(inner, "x-composite", {
598
+ configurable: true,
599
+ enumerable: true,
600
+ writable: true,
601
+ value: type
602
+ });
603
+ if (context.definitions) {
604
+ context.definitions[type] = inner;
605
+ }
606
+ }
607
+ const wrapper = handler.wrapRef(ref, normalizedField);
608
+ Object.defineProperty(wrapper, "x-composite", {
609
+ configurable: true,
610
+ enumerable: true,
611
+ writable: true,
612
+ value: type
613
+ });
614
+ return wrapper;
615
+ }
280
616
  const result = {
281
617
  type: FORMAN_TYPE_MAP[normalizedField.type],
282
618
  title: noEmpty(normalizedField.label),
@@ -694,6 +1030,8 @@ var FORMAN_TYPE_MAP2 = {
694
1030
  pkey: "string",
695
1031
  port: "number",
696
1032
  select: void 0,
1033
+ udttype: "string",
1034
+ udtspec: "array",
697
1035
  time: "string",
698
1036
  timestamp: "string",
699
1037
  timezone: "string",
@@ -766,6 +1104,25 @@ async function validateFormanWithDomainsInternal(domains, options) {
766
1104
  }
767
1105
  );
768
1106
  errors.push(...result.errors);
1107
+ const restoreExtras = options?.states && domains[domain]?.restoreExtras;
1108
+ if (restoreExtras) {
1109
+ const fieldStateMap = new Map(
1110
+ roots[domain].fieldStates.map(({ path, state }) => {
1111
+ return [pathToString(path), state];
1112
+ })
1113
+ );
1114
+ for (const [fieldPath, extra] of Object.entries(restoreExtras)) {
1115
+ const state = fieldStateMap.get(fieldPath);
1116
+ if (state) {
1117
+ state.extra = extra;
1118
+ } else {
1119
+ roots[domain].fieldStates.push({
1120
+ path: stringToPath(fieldPath),
1121
+ state: { extra }
1122
+ });
1123
+ }
1124
+ }
1125
+ }
769
1126
  }
770
1127
  return {
771
1128
  valid: errors.length === 0,
@@ -834,6 +1191,12 @@ async function validateFormanValue(value, field, context) {
834
1191
  errors: []
835
1192
  };
836
1193
  }
1194
+ if (normalizedField.type === "udttype") {
1195
+ return validateFormanValue(value, udttypeExpand({ ...normalizedField }), context);
1196
+ }
1197
+ if (normalizedField.type === "udtspec") {
1198
+ return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1199
+ }
837
1200
  switch (normalizedField.type) {
838
1201
  case "collection":
839
1202
  return handleCollectionType2(value, normalizedField, context);
@@ -1183,7 +1546,21 @@ async function handleSelectType(value, field, context) {
1183
1546
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
1184
1547
  if (typeof optionsOrGroups === "string") {
1185
1548
  try {
1186
- optionsOrGroups = await context.resolveRemote(optionsOrGroups, context);
1549
+ const resolved = await context.resolveRemote(optionsOrGroups, context);
1550
+ if (resolved == null || typeof resolved !== "object") {
1551
+ return {
1552
+ valid: false,
1553
+ errors: [
1554
+ ...errors,
1555
+ {
1556
+ domain: context.domain,
1557
+ path: context.path.join("."),
1558
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1559
+ }
1560
+ ]
1561
+ };
1562
+ }
1563
+ optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1187
1564
  } catch (error) {
1188
1565
  return {
1189
1566
  valid: false,
@@ -1293,7 +1670,12 @@ async function handleNestedFields(nested, value, field, context) {
1293
1670
  for (const item of store) {
1294
1671
  if (typeof item === "string") {
1295
1672
  try {
1296
- resolvedStore.push(...await context.resolveRemote(item, context));
1673
+ const resolved = await context.resolveRemote(item, context);
1674
+ if (Array.isArray(resolved)) {
1675
+ resolvedStore.push(...resolved);
1676
+ } else if (resolved && typeof resolved === "object") {
1677
+ resolvedStore.push(resolved);
1678
+ }
1297
1679
  } catch (error) {
1298
1680
  return {
1299
1681
  valid: false,
@@ -1401,6 +1783,9 @@ var JSON_PRIMITIVE_TYPE_MAP = {
1401
1783
  boolean: "boolean"
1402
1784
  };
1403
1785
  function toFormanSchema(field) {
1786
+ const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
1787
+ if (compositeType === "udttype") return udttypeCollapse(field);
1788
+ if (compositeType === "udtspec") return udtspecCollapse(field);
1404
1789
  switch (field.type) {
1405
1790
  case "object":
1406
1791
  if (!field.properties || !Object.entries(field.properties).length) {
@@ -1523,13 +1908,23 @@ function handleSearchDirective(formanField, directive) {
1523
1908
 
1524
1909
  // src/index.ts
1525
1910
  function toJSONSchema(field) {
1526
- return toJSONSchemaInternal(field);
1911
+ const context = createDefaultContext();
1912
+ const result = toJSONSchemaInternal(field, context);
1913
+ if (Object.keys(context.definitions ?? {}).length > 0) {
1914
+ Object.defineProperty(result, "definitions", {
1915
+ configurable: true,
1916
+ enumerable: true,
1917
+ writable: true,
1918
+ value: context.definitions
1919
+ });
1920
+ }
1921
+ return result;
1527
1922
  }
1528
1923
  function validateFormanWithDomains(domains, options) {
1529
1924
  return validateFormanWithDomainsInternal(domains, options);
1530
1925
  }
1531
- function validateForman(values, schema, options) {
1532
- return validateFormanWithDomains({ default: { values, schema } }, options);
1926
+ function validateForman(values, schema, options, restoreExtras) {
1927
+ return validateFormanWithDomains({ default: { values, schema, restoreExtras } }, options);
1533
1928
  }
1534
1929
  export {
1535
1930
  toFormanSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.8.3",
3
+ "version": "1.9.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",