@makehq/forman-schema 1.8.2 → 1.8.4

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
@@ -223,6 +223,248 @@ var IML_BINARY_FILTER_OPERATORS = [
223
223
  ];
224
224
  var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
225
225
 
226
+ // src/composites/udttype.ts
227
+ function udttypeExpand(field) {
228
+ field.type = "select";
229
+ field.options = {
230
+ store: [
231
+ {
232
+ label: "Array",
233
+ value: "array",
234
+ nested: [
235
+ {
236
+ name: "spec",
237
+ type: "collection",
238
+ label: "Array Item Specification",
239
+ spec: [
240
+ {
241
+ name: "type",
242
+ label: "Type",
243
+ type: "udttype",
244
+ required: true,
245
+ default: "text"
246
+ }
247
+ ],
248
+ mappable: false
249
+ },
250
+ {
251
+ name: "required",
252
+ label: "Required",
253
+ type: "boolean",
254
+ required: true,
255
+ default: false,
256
+ mappable: false
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ label: "Collection",
262
+ value: "collection",
263
+ nested: [
264
+ {
265
+ name: "spec",
266
+ label: "Specification",
267
+ type: "udtspec"
268
+ },
269
+ {
270
+ name: "sequence",
271
+ label: "Preserve the order of object keys",
272
+ type: "boolean",
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: "Date",
287
+ value: "date",
288
+ nested: [
289
+ {
290
+ name: "required",
291
+ label: "Required",
292
+ type: "boolean",
293
+ required: true,
294
+ default: false,
295
+ mappable: false
296
+ }
297
+ ]
298
+ },
299
+ {
300
+ label: "Text",
301
+ value: "text",
302
+ nested: [
303
+ {
304
+ name: "default",
305
+ label: "Default value",
306
+ placeholder: "Enter default value",
307
+ type: "text"
308
+ },
309
+ {
310
+ name: "required",
311
+ label: "Required",
312
+ type: "boolean",
313
+ required: true,
314
+ default: false,
315
+ mappable: false
316
+ },
317
+ {
318
+ name: "multiline",
319
+ label: "Multi-line",
320
+ type: "boolean",
321
+ required: true,
322
+ default: false,
323
+ mappable: false
324
+ }
325
+ ]
326
+ },
327
+ {
328
+ label: "Number",
329
+ value: "number",
330
+ nested: [
331
+ {
332
+ name: "default",
333
+ label: "Default value",
334
+ placeholder: "Enter default value",
335
+ type: "number"
336
+ },
337
+ {
338
+ name: "required",
339
+ label: "Required",
340
+ type: "boolean",
341
+ required: true,
342
+ default: false,
343
+ mappable: false
344
+ }
345
+ ]
346
+ },
347
+ {
348
+ label: "Boolean",
349
+ value: "boolean",
350
+ nested: [
351
+ {
352
+ name: "default",
353
+ label: "Default value",
354
+ placeholder: "Enter default value",
355
+ type: "boolean",
356
+ mappable: false
357
+ },
358
+ {
359
+ name: "required",
360
+ label: "Required",
361
+ type: "boolean",
362
+ required: true,
363
+ default: false,
364
+ mappable: false
365
+ }
366
+ ]
367
+ },
368
+ {
369
+ label: "Binary Data",
370
+ value: "buffer",
371
+ nested: [
372
+ {
373
+ name: "required",
374
+ label: "Required",
375
+ type: "boolean",
376
+ required: true,
377
+ default: false,
378
+ mappable: false
379
+ },
380
+ {
381
+ name: "codepage",
382
+ label: "Codepage",
383
+ type: "text",
384
+ help: "Possible values: `binary`, `utf8`. Leave empty if you're not sure."
385
+ }
386
+ ]
387
+ }
388
+ ]
389
+ };
390
+ return field;
391
+ }
392
+ function udttypeExtractInner(schema) {
393
+ const { title, description, ...inner } = schema;
394
+ return inner;
395
+ }
396
+ function udttypeWrapRef(ref, field) {
397
+ return {
398
+ allOf: [{ $ref: ref }],
399
+ title: noEmpty(field.label),
400
+ description: noEmpty(field.help),
401
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
402
+ };
403
+ }
404
+ function udttypeCollapse(field) {
405
+ return {
406
+ type: "udttype",
407
+ label: noEmpty(field.title),
408
+ help: noEmpty(field.description),
409
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
410
+ };
411
+ }
412
+
413
+ // src/composites/udtspec.ts
414
+ function udtspecExpand(field) {
415
+ field.type = "array";
416
+ field.spec = [
417
+ {
418
+ name: "name",
419
+ label: "Name",
420
+ placeholder: "Enter name",
421
+ type: "text",
422
+ required: true
423
+ },
424
+ {
425
+ name: "label",
426
+ label: "Label",
427
+ help: "Display name for better readability.",
428
+ type: "text",
429
+ advanced: true
430
+ },
431
+ {
432
+ name: "help",
433
+ label: "Description",
434
+ type: "text",
435
+ multiline: true,
436
+ required: false,
437
+ placeholder: "Enter description"
438
+ },
439
+ {
440
+ name: "type",
441
+ label: "Type",
442
+ type: "udttype",
443
+ required: true,
444
+ default: "text"
445
+ }
446
+ ];
447
+ return field;
448
+ }
449
+ function udtspecExtractInner(schema) {
450
+ return schema.items;
451
+ }
452
+ function udtspecWrapRef(ref, field) {
453
+ return {
454
+ type: "array",
455
+ title: noEmpty(field.label),
456
+ description: noEmpty(field.help),
457
+ items: { $ref: ref }
458
+ };
459
+ }
460
+ function udtspecCollapse(field) {
461
+ return {
462
+ type: "udtspec",
463
+ label: noEmpty(field.title),
464
+ help: noEmpty(field.description)
465
+ };
466
+ }
467
+
226
468
  // src/forman.ts
227
469
  var SchemaConversionError = class extends Error {
228
470
  /** Field that caused the error */
@@ -269,6 +511,8 @@ var FORMAN_TYPE_MAP = {
269
511
  pkey: "string",
270
512
  port: "number",
271
513
  select: "string",
514
+ udttype: "string",
515
+ udtspec: "array",
272
516
  time: "string",
273
517
  timestamp: "string",
274
518
  timezone: "string",
@@ -298,14 +542,52 @@ function createDefaultContext() {
298
542
  tail: [],
299
543
  path: [],
300
544
  roots: {},
545
+ definitions: {},
301
546
  addConditionalFields: () => {
302
547
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
303
548
  }
304
549
  };
305
550
  }
306
- function toJSONSchemaInternal(field, context = createDefaultContext()) {
551
+ var compositeHandlers = {
552
+ udtspec: { expand: udtspecExpand, extractInner: udtspecExtractInner, wrapRef: udtspecWrapRef },
553
+ udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
554
+ };
555
+ function toJSONSchemaInternal(field, context) {
307
556
  validateFormanField(field);
308
557
  const normalizedField = normalizeFormanFieldType(field);
558
+ const handler = compositeHandlers[normalizedField.type];
559
+ if (handler) {
560
+ const type = normalizedField.type;
561
+ const ref = `#/definitions/${type}`;
562
+ if (!context.definitions?.[type]) {
563
+ if (context.definitions) {
564
+ context.definitions[type] = {};
565
+ }
566
+ const expandField = { ...normalizedField };
567
+ delete expandField.label;
568
+ delete expandField.help;
569
+ const expanded = handler.expand(expandField);
570
+ const expandedResult = toJSONSchemaInternal(expanded, context);
571
+ const inner = handler.extractInner(expandedResult);
572
+ Object.defineProperty(inner, "x-composite", {
573
+ configurable: true,
574
+ enumerable: true,
575
+ writable: true,
576
+ value: type
577
+ });
578
+ if (context.definitions) {
579
+ context.definitions[type] = inner;
580
+ }
581
+ }
582
+ const wrapper = handler.wrapRef(ref, normalizedField);
583
+ Object.defineProperty(wrapper, "x-composite", {
584
+ configurable: true,
585
+ enumerable: true,
586
+ writable: true,
587
+ value: type
588
+ });
589
+ return wrapper;
590
+ }
309
591
  const result = {
310
592
  type: FORMAN_TYPE_MAP[normalizedField.type],
311
593
  title: noEmpty(normalizedField.label),
@@ -412,31 +694,92 @@ function handleArrayType(field, result, context) {
412
694
  return result;
413
695
  }
414
696
  function handleFilterType(field, result, context) {
415
- const filterItems = {
697
+ const operandOptions = isObject(field.options) ? field.options.store : field.options;
698
+ const operandSchema = {
699
+ title: "Operand A",
700
+ description: "The first operand of the filter."
701
+ };
702
+ if (Array.isArray(operandOptions)) {
703
+ operandSchema.type = "string";
704
+ operandSchema.oneOf = operandOptions.flatMap((optionOrGroup) => {
705
+ if (isOptionGroup(optionOrGroup)) {
706
+ return optionOrGroup.options.map((option) => ({
707
+ title: `${optionOrGroup.label}: ${option.label || option.value}`,
708
+ const: option.value
709
+ }));
710
+ }
711
+ return {
712
+ title: optionOrGroup.label || String(optionOrGroup.value),
713
+ const: optionOrGroup.value
714
+ };
715
+ });
716
+ } else if (typeof operandOptions === "string") {
717
+ operandSchema.type = "string";
718
+ Object.defineProperty(operandSchema, "x-fetch", {
719
+ configurable: true,
720
+ enumerable: true,
721
+ writable: true,
722
+ value: appendQueryString(operandOptions, context.domain, context.tail)
723
+ });
724
+ } else {
725
+ operandSchema.type = IML_FILTER_ENTRY_TYPES;
726
+ }
727
+ const operatorOptions = isObject(field.options) ? field.options.operators : void 0;
728
+ const operatorSchema = {
729
+ title: "Operator",
730
+ description: "The operator of the filter."
731
+ };
732
+ if (Array.isArray(operatorOptions)) {
733
+ operatorSchema.type = "string";
734
+ operatorSchema.oneOf = operatorOptions.flatMap((optionOrGroup) => {
735
+ if (isOptionGroup(optionOrGroup)) {
736
+ return optionOrGroup.options.map((option) => ({
737
+ title: `${optionOrGroup.label}: ${option.label || option.value}`,
738
+ const: option.value
739
+ }));
740
+ }
741
+ return {
742
+ title: optionOrGroup.label || String(optionOrGroup.value),
743
+ const: optionOrGroup.value
744
+ };
745
+ });
746
+ } else {
747
+ operatorSchema.enum = IML_BINARY_FILTER_OPERATORS;
748
+ }
749
+ const filterDefinition = {
416
750
  oneOf: [
417
751
  {
418
752
  type: "object",
419
753
  properties: {
420
- a: { type: IML_FILTER_ENTRY_TYPES },
421
- o: { enum: IML_UNARY_FILTER_OPERATORS }
422
- },
423
- required: ["a", "o"]
424
- },
425
- {
426
- type: "object",
427
- properties: {
428
- a: { type: IML_FILTER_ENTRY_TYPES },
429
- b: { type: IML_FILTER_ENTRY_TYPES },
430
- o: { enum: IML_BINARY_FILTER_OPERATORS }
754
+ a: operandSchema,
755
+ o: operatorSchema,
756
+ b: {
757
+ title: "Operand B",
758
+ description: "The second operand of the filter.",
759
+ type: IML_FILTER_ENTRY_TYPES
760
+ }
431
761
  },
432
762
  required: ["a", "b", "o"]
433
763
  }
434
764
  ]
435
765
  };
766
+ if (!operatorOptions) {
767
+ filterDefinition.oneOf?.push({
768
+ type: "object",
769
+ properties: {
770
+ a: operandSchema,
771
+ o: {
772
+ ...operatorSchema,
773
+ enum: IML_UNARY_FILTER_OPERATORS
774
+ }
775
+ },
776
+ required: ["a", "o"]
777
+ });
778
+ }
436
779
  const logic = field.logic ?? "default";
437
- result.items = ["and", "or"].includes(logic) ? filterItems : {
780
+ result.items = ["and", "or"].includes(logic) ? filterDefinition : {
438
781
  type: "array",
439
- items: filterItems
782
+ items: filterDefinition
440
783
  };
441
784
  Object.defineProperty(result, "x-filter", {
442
785
  configurable: true,
@@ -462,9 +805,7 @@ function handleSelectOrPathType(field, result, context) {
462
805
  }
463
806
  });
464
807
  }
465
- const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
466
- const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
467
- const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
808
+ const { nested, domain } = extractNestedAndDomain(field);
468
809
  if (typeof optionsOrGroups === "string") {
469
810
  Object.defineProperty(result, "x-fetch", {
470
811
  configurable: true,
@@ -533,46 +874,7 @@ function handleSelectOrPathType(field, result, context) {
533
874
  result.enum = (options || []).map((option) => option.value);
534
875
  }
535
876
  }
536
- if (nested && domain && domain !== context.domain) {
537
- const normalizedNested = typeof nested === "string" ? [nested] : nested;
538
- let root = context.roots[domain];
539
- if (!root) {
540
- const buffer = [];
541
- root = context.roots[domain] = {
542
- buffer,
543
- addFields: (nested2, tail) => {
544
- buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
545
- }
546
- };
547
- }
548
- root.addFields(normalizedNested, [...context.tail, field.name]);
549
- } else if (nested) {
550
- Object.defineProperty(result, "x-nested", {
551
- configurable: true,
552
- enumerable: true,
553
- writable: true,
554
- value: typeof nested === "string" ? { $ref: appendQueryString(nested, context.domain, [...context.tail, field.name]) } : nestedContainsStrings ? {
555
- type: "object",
556
- allOf: nested.map(
557
- (item) => typeof item === "string" ? { $ref: appendQueryString(item, context.domain, [...context.tail, field.name]) } : toJSONSchemaInternal(
558
- { type: "collection", spec: [item] },
559
- {
560
- ...context,
561
- domain: domain || context.domain,
562
- tail: [...context.tail, field.name]
563
- }
564
- )
565
- )
566
- } : toJSONSchemaInternal(
567
- { type: "collection", spec: nested },
568
- {
569
- ...context,
570
- domain: domain || context.domain,
571
- tail: [...context.tail, field.name]
572
- }
573
- )
574
- });
575
- }
877
+ result = handleNestedWithDomain(field, nested, domain, result, context);
576
878
  if (field.rpc) result = processRpcDirective(field, result, context);
577
879
  return result;
578
880
  }
@@ -595,6 +897,10 @@ function handlePrimitiveType(field, result, context) {
595
897
  }
596
898
  }
597
899
  if (field.rpc) result = processRpcDirective(field, result, context);
900
+ if (field.nested) {
901
+ const { nested, domain } = extractNestedAndDomain(field);
902
+ result = handleNestedWithDomain(field, nested, domain, result, context);
903
+ }
598
904
  return result;
599
905
  }
600
906
  function processRpcDirective(field, result, context) {
@@ -611,6 +917,60 @@ function processRpcDirective(field, result, context) {
611
917
  });
612
918
  return result;
613
919
  }
920
+ function extractNestedAndDomain(field) {
921
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isObject(field.nested) ? field.nested.store : field.nested;
922
+ const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : isObject(field.nested) && field.nested.domain ? field.nested.domain : void 0;
923
+ return { nested, domain };
924
+ }
925
+ function handleNestedWithDomain(field, nested, domain, result, context) {
926
+ if (!nested) return result;
927
+ if (domain && domain !== context.domain) {
928
+ const normalizedNested = typeof nested === "string" ? [nested] : nested;
929
+ let root = context.roots[domain];
930
+ if (!root) {
931
+ const buffer = [];
932
+ root = context.roots[domain] = {
933
+ buffer,
934
+ addFields: (nested2, tail) => {
935
+ buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
936
+ }
937
+ };
938
+ }
939
+ root.addFields(normalizedNested, [...context.tail, field.name]);
940
+ return result;
941
+ }
942
+ return processNestedDirective(field, nested, domain, result, context);
943
+ }
944
+ function processNestedDirective(field, nested, domain, result, context) {
945
+ if (!nested) return result;
946
+ const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
947
+ Object.defineProperty(result, "x-nested", {
948
+ configurable: true,
949
+ enumerable: true,
950
+ writable: true,
951
+ value: typeof nested === "string" ? { $ref: appendQueryString(nested, context.domain, [...context.tail, field.name]) } : nestedContainsStrings ? {
952
+ type: "object",
953
+ allOf: nested.map(
954
+ (item) => typeof item === "string" ? { $ref: appendQueryString(item, context.domain, [...context.tail, field.name]) } : toJSONSchemaInternal(
955
+ { type: "collection", spec: [item] },
956
+ {
957
+ ...context,
958
+ domain: domain || context.domain,
959
+ tail: [...context.tail, field.name]
960
+ }
961
+ )
962
+ )
963
+ } : toJSONSchemaInternal(
964
+ { type: "collection", spec: nested },
965
+ {
966
+ ...context,
967
+ domain: domain || context.domain,
968
+ tail: [...context.tail, field.name]
969
+ }
970
+ )
971
+ });
972
+ return result;
973
+ }
614
974
 
615
975
  // src/validator.ts
616
976
  var FORMAN_TYPE_MAP2 = {
@@ -645,6 +1005,8 @@ var FORMAN_TYPE_MAP2 = {
645
1005
  pkey: "string",
646
1006
  port: "number",
647
1007
  select: void 0,
1008
+ udttype: "string",
1009
+ udtspec: "array",
648
1010
  time: "string",
649
1011
  timestamp: "string",
650
1012
  timezone: "string",
@@ -785,6 +1147,12 @@ async function validateFormanValue(value, field, context) {
785
1147
  errors: []
786
1148
  };
787
1149
  }
1150
+ if (normalizedField.type === "udttype") {
1151
+ return validateFormanValue(value, udttypeExpand({ ...normalizedField }), context);
1152
+ }
1153
+ if (normalizedField.type === "udtspec") {
1154
+ return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1155
+ }
788
1156
  switch (normalizedField.type) {
789
1157
  case "collection":
790
1158
  return handleCollectionType2(value, normalizedField, context);
@@ -932,24 +1300,40 @@ async function handleArrayType2(value, field, context) {
932
1300
  };
933
1301
  }
934
1302
  async function handleFilterType2(value, field, context) {
1303
+ const operandOptions = isObject(field.options) ? field.options.store : field.options;
1304
+ const operandSpec = {
1305
+ name: "a",
1306
+ type: "any",
1307
+ required: true
1308
+ };
1309
+ if (Array.isArray(operandOptions)) {
1310
+ operandSpec.type = "select";
1311
+ operandSpec.options = operandOptions;
1312
+ }
1313
+ const operatorOptions = isObject(field.options) ? field.options.operators : void 0;
1314
+ const operatorSpec = {
1315
+ name: "o",
1316
+ type: "any",
1317
+ required: true
1318
+ };
1319
+ if (Array.isArray(operatorOptions)) {
1320
+ operatorSpec.type = "select";
1321
+ operatorSpec.grouped = true;
1322
+ operatorSpec.options = operatorOptions;
1323
+ } else {
1324
+ operatorSpec.type = "text";
1325
+ operatorSpec.validate = {
1326
+ enum: IML_FILTER_OPERATORS
1327
+ };
1328
+ }
935
1329
  const filterEntry = {
936
1330
  type: "collection",
937
1331
  spec: [
938
- {
939
- name: "a",
940
- type: "any",
941
- required: true
942
- },
1332
+ operandSpec,
1333
+ operatorSpec,
943
1334
  {
944
1335
  name: "b",
945
1336
  type: "any"
946
- },
947
- {
948
- name: "o",
949
- type: "text",
950
- validate: {
951
- enum: IML_FILTER_OPERATORS
952
- }
953
1337
  }
954
1338
  ]
955
1339
  };
@@ -1118,7 +1502,18 @@ async function handleSelectType(value, field, context) {
1118
1502
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
1119
1503
  if (typeof optionsOrGroups === "string") {
1120
1504
  try {
1121
- optionsOrGroups = await context.resolveRemote(optionsOrGroups, context);
1505
+ const resolved = await context.resolveRemote(optionsOrGroups, context);
1506
+ if (resolved == null || typeof resolved !== "object") {
1507
+ return {
1508
+ valid: false,
1509
+ errors: [...errors, {
1510
+ domain: context.domain,
1511
+ path: context.path.join("."),
1512
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1513
+ }]
1514
+ };
1515
+ }
1516
+ optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1122
1517
  } catch (error) {
1123
1518
  return {
1124
1519
  valid: false,
@@ -1228,7 +1623,12 @@ async function handleNestedFields(nested, value, field, context) {
1228
1623
  for (const item of store) {
1229
1624
  if (typeof item === "string") {
1230
1625
  try {
1231
- resolvedStore.push(...await context.resolveRemote(item, context));
1626
+ const resolved = await context.resolveRemote(item, context);
1627
+ if (Array.isArray(resolved)) {
1628
+ resolvedStore.push(...resolved);
1629
+ } else if (resolved && typeof resolved === "object") {
1630
+ resolvedStore.push(resolved);
1631
+ }
1232
1632
  } catch (error) {
1233
1633
  return {
1234
1634
  valid: false,
@@ -1336,6 +1736,9 @@ var JSON_PRIMITIVE_TYPE_MAP = {
1336
1736
  boolean: "boolean"
1337
1737
  };
1338
1738
  function toFormanSchema(field) {
1739
+ const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
1740
+ if (compositeType === "udttype") return udttypeCollapse(field);
1741
+ if (compositeType === "udtspec") return udtspecCollapse(field);
1339
1742
  switch (field.type) {
1340
1743
  case "object":
1341
1744
  if (!field.properties || !Object.entries(field.properties).length) {
@@ -1458,7 +1861,17 @@ function handleSearchDirective(formanField, directive) {
1458
1861
 
1459
1862
  // src/index.ts
1460
1863
  function toJSONSchema(field) {
1461
- return toJSONSchemaInternal(field);
1864
+ const context = createDefaultContext();
1865
+ const result = toJSONSchemaInternal(field, context);
1866
+ if (Object.keys(context.definitions ?? {}).length > 0) {
1867
+ Object.defineProperty(result, "definitions", {
1868
+ configurable: true,
1869
+ enumerable: true,
1870
+ writable: true,
1871
+ value: context.definitions
1872
+ });
1873
+ }
1874
+ return result;
1462
1875
  }
1463
1876
  function validateFormanWithDomains(domains, options) {
1464
1877
  return validateFormanWithDomainsInternal(domains, options);
package/dist/index.d.cts CHANGED
@@ -145,6 +145,10 @@ type FormanSchemaOptionGroup = {
145
145
  /** Group options */
146
146
  options: FormanSchemaOption[];
147
147
  };
148
+ /**
149
+ * Helper type for store of select, where options can be either plain options, or groups
150
+ */
151
+ type FormanSchemaSelectOptionsStore = (FormanSchemaOption | FormanSchemaOptionGroup)[];
148
152
  /**
149
153
  * Extended options for a select field
150
154
  */
@@ -162,6 +166,8 @@ type FormanSchemaExtendedOptions = {
162
166
  label: string;
163
167
  nested?: FormanSchemaNested;
164
168
  };
169
+ /** Definition of field operators, applicable on Filter fields. */
170
+ operators?: FormanSchemaSelectOptionsStore;
165
171
  };
166
172
  /**
167
173
  * Nested fields
package/dist/index.d.ts CHANGED
@@ -145,6 +145,10 @@ type FormanSchemaOptionGroup = {
145
145
  /** Group options */
146
146
  options: FormanSchemaOption[];
147
147
  };
148
+ /**
149
+ * Helper type for store of select, where options can be either plain options, or groups
150
+ */
151
+ type FormanSchemaSelectOptionsStore = (FormanSchemaOption | FormanSchemaOptionGroup)[];
148
152
  /**
149
153
  * Extended options for a select field
150
154
  */
@@ -162,6 +166,8 @@ type FormanSchemaExtendedOptions = {
162
166
  label: string;
163
167
  nested?: FormanSchemaNested;
164
168
  };
169
+ /** Definition of field operators, applicable on Filter fields. */
170
+ operators?: FormanSchemaSelectOptionsStore;
165
171
  };
166
172
  /**
167
173
  * Nested fields
package/dist/index.js CHANGED
@@ -194,6 +194,248 @@ var IML_BINARY_FILTER_OPERATORS = [
194
194
  ];
195
195
  var IML_FILTER_OPERATORS = [...IML_UNARY_FILTER_OPERATORS, ...IML_BINARY_FILTER_OPERATORS];
196
196
 
197
+ // src/composites/udttype.ts
198
+ function udttypeExpand(field) {
199
+ field.type = "select";
200
+ field.options = {
201
+ store: [
202
+ {
203
+ label: "Array",
204
+ value: "array",
205
+ nested: [
206
+ {
207
+ name: "spec",
208
+ type: "collection",
209
+ label: "Array Item Specification",
210
+ spec: [
211
+ {
212
+ name: "type",
213
+ label: "Type",
214
+ type: "udttype",
215
+ required: true,
216
+ default: "text"
217
+ }
218
+ ],
219
+ mappable: false
220
+ },
221
+ {
222
+ name: "required",
223
+ label: "Required",
224
+ type: "boolean",
225
+ required: true,
226
+ default: false,
227
+ mappable: false
228
+ }
229
+ ]
230
+ },
231
+ {
232
+ label: "Collection",
233
+ value: "collection",
234
+ nested: [
235
+ {
236
+ name: "spec",
237
+ label: "Specification",
238
+ type: "udtspec"
239
+ },
240
+ {
241
+ name: "sequence",
242
+ label: "Preserve the order of object keys",
243
+ type: "boolean",
244
+ mappable: false
245
+ },
246
+ {
247
+ name: "required",
248
+ label: "Required",
249
+ type: "boolean",
250
+ required: true,
251
+ default: false,
252
+ mappable: false
253
+ }
254
+ ]
255
+ },
256
+ {
257
+ label: "Date",
258
+ value: "date",
259
+ nested: [
260
+ {
261
+ name: "required",
262
+ label: "Required",
263
+ type: "boolean",
264
+ required: true,
265
+ default: false,
266
+ mappable: false
267
+ }
268
+ ]
269
+ },
270
+ {
271
+ label: "Text",
272
+ value: "text",
273
+ nested: [
274
+ {
275
+ name: "default",
276
+ label: "Default value",
277
+ placeholder: "Enter default value",
278
+ type: "text"
279
+ },
280
+ {
281
+ name: "required",
282
+ label: "Required",
283
+ type: "boolean",
284
+ required: true,
285
+ default: false,
286
+ mappable: false
287
+ },
288
+ {
289
+ name: "multiline",
290
+ label: "Multi-line",
291
+ type: "boolean",
292
+ required: true,
293
+ default: false,
294
+ mappable: false
295
+ }
296
+ ]
297
+ },
298
+ {
299
+ label: "Number",
300
+ value: "number",
301
+ nested: [
302
+ {
303
+ name: "default",
304
+ label: "Default value",
305
+ placeholder: "Enter default value",
306
+ type: "number"
307
+ },
308
+ {
309
+ name: "required",
310
+ label: "Required",
311
+ type: "boolean",
312
+ required: true,
313
+ default: false,
314
+ mappable: false
315
+ }
316
+ ]
317
+ },
318
+ {
319
+ label: "Boolean",
320
+ value: "boolean",
321
+ nested: [
322
+ {
323
+ name: "default",
324
+ label: "Default value",
325
+ placeholder: "Enter default value",
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: "Binary Data",
341
+ value: "buffer",
342
+ nested: [
343
+ {
344
+ name: "required",
345
+ label: "Required",
346
+ type: "boolean",
347
+ required: true,
348
+ default: false,
349
+ mappable: false
350
+ },
351
+ {
352
+ name: "codepage",
353
+ label: "Codepage",
354
+ type: "text",
355
+ help: "Possible values: `binary`, `utf8`. Leave empty if you're not sure."
356
+ }
357
+ ]
358
+ }
359
+ ]
360
+ };
361
+ return field;
362
+ }
363
+ function udttypeExtractInner(schema) {
364
+ const { title, description, ...inner } = schema;
365
+ return inner;
366
+ }
367
+ function udttypeWrapRef(ref, field) {
368
+ return {
369
+ allOf: [{ $ref: ref }],
370
+ title: noEmpty(field.label),
371
+ description: noEmpty(field.help),
372
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
373
+ };
374
+ }
375
+ function udttypeCollapse(field) {
376
+ return {
377
+ type: "udttype",
378
+ label: noEmpty(field.title),
379
+ help: noEmpty(field.description),
380
+ ...field.default !== "" && field.default != null ? { default: field.default } : {}
381
+ };
382
+ }
383
+
384
+ // src/composites/udtspec.ts
385
+ function udtspecExpand(field) {
386
+ field.type = "array";
387
+ field.spec = [
388
+ {
389
+ name: "name",
390
+ label: "Name",
391
+ placeholder: "Enter name",
392
+ type: "text",
393
+ required: true
394
+ },
395
+ {
396
+ name: "label",
397
+ label: "Label",
398
+ help: "Display name for better readability.",
399
+ type: "text",
400
+ advanced: true
401
+ },
402
+ {
403
+ name: "help",
404
+ label: "Description",
405
+ type: "text",
406
+ multiline: true,
407
+ required: false,
408
+ placeholder: "Enter description"
409
+ },
410
+ {
411
+ name: "type",
412
+ label: "Type",
413
+ type: "udttype",
414
+ required: true,
415
+ default: "text"
416
+ }
417
+ ];
418
+ return field;
419
+ }
420
+ function udtspecExtractInner(schema) {
421
+ return schema.items;
422
+ }
423
+ function udtspecWrapRef(ref, field) {
424
+ return {
425
+ type: "array",
426
+ title: noEmpty(field.label),
427
+ description: noEmpty(field.help),
428
+ items: { $ref: ref }
429
+ };
430
+ }
431
+ function udtspecCollapse(field) {
432
+ return {
433
+ type: "udtspec",
434
+ label: noEmpty(field.title),
435
+ help: noEmpty(field.description)
436
+ };
437
+ }
438
+
197
439
  // src/forman.ts
198
440
  var SchemaConversionError = class extends Error {
199
441
  /** Field that caused the error */
@@ -240,6 +482,8 @@ var FORMAN_TYPE_MAP = {
240
482
  pkey: "string",
241
483
  port: "number",
242
484
  select: "string",
485
+ udttype: "string",
486
+ udtspec: "array",
243
487
  time: "string",
244
488
  timestamp: "string",
245
489
  timezone: "string",
@@ -269,14 +513,52 @@ function createDefaultContext() {
269
513
  tail: [],
270
514
  path: [],
271
515
  roots: {},
516
+ definitions: {},
272
517
  addConditionalFields: () => {
273
518
  throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
274
519
  }
275
520
  };
276
521
  }
277
- function toJSONSchemaInternal(field, context = createDefaultContext()) {
522
+ var compositeHandlers = {
523
+ udtspec: { expand: udtspecExpand, extractInner: udtspecExtractInner, wrapRef: udtspecWrapRef },
524
+ udttype: { expand: udttypeExpand, extractInner: udttypeExtractInner, wrapRef: udttypeWrapRef }
525
+ };
526
+ function toJSONSchemaInternal(field, context) {
278
527
  validateFormanField(field);
279
528
  const normalizedField = normalizeFormanFieldType(field);
529
+ const handler = compositeHandlers[normalizedField.type];
530
+ if (handler) {
531
+ const type = normalizedField.type;
532
+ const ref = `#/definitions/${type}`;
533
+ if (!context.definitions?.[type]) {
534
+ if (context.definitions) {
535
+ context.definitions[type] = {};
536
+ }
537
+ const expandField = { ...normalizedField };
538
+ delete expandField.label;
539
+ delete expandField.help;
540
+ const expanded = handler.expand(expandField);
541
+ const expandedResult = toJSONSchemaInternal(expanded, context);
542
+ const inner = handler.extractInner(expandedResult);
543
+ Object.defineProperty(inner, "x-composite", {
544
+ configurable: true,
545
+ enumerable: true,
546
+ writable: true,
547
+ value: type
548
+ });
549
+ if (context.definitions) {
550
+ context.definitions[type] = inner;
551
+ }
552
+ }
553
+ const wrapper = handler.wrapRef(ref, normalizedField);
554
+ Object.defineProperty(wrapper, "x-composite", {
555
+ configurable: true,
556
+ enumerable: true,
557
+ writable: true,
558
+ value: type
559
+ });
560
+ return wrapper;
561
+ }
280
562
  const result = {
281
563
  type: FORMAN_TYPE_MAP[normalizedField.type],
282
564
  title: noEmpty(normalizedField.label),
@@ -383,31 +665,92 @@ function handleArrayType(field, result, context) {
383
665
  return result;
384
666
  }
385
667
  function handleFilterType(field, result, context) {
386
- const filterItems = {
668
+ const operandOptions = isObject(field.options) ? field.options.store : field.options;
669
+ const operandSchema = {
670
+ title: "Operand A",
671
+ description: "The first operand of the filter."
672
+ };
673
+ if (Array.isArray(operandOptions)) {
674
+ operandSchema.type = "string";
675
+ operandSchema.oneOf = operandOptions.flatMap((optionOrGroup) => {
676
+ if (isOptionGroup(optionOrGroup)) {
677
+ return optionOrGroup.options.map((option) => ({
678
+ title: `${optionOrGroup.label}: ${option.label || option.value}`,
679
+ const: option.value
680
+ }));
681
+ }
682
+ return {
683
+ title: optionOrGroup.label || String(optionOrGroup.value),
684
+ const: optionOrGroup.value
685
+ };
686
+ });
687
+ } else if (typeof operandOptions === "string") {
688
+ operandSchema.type = "string";
689
+ Object.defineProperty(operandSchema, "x-fetch", {
690
+ configurable: true,
691
+ enumerable: true,
692
+ writable: true,
693
+ value: appendQueryString(operandOptions, context.domain, context.tail)
694
+ });
695
+ } else {
696
+ operandSchema.type = IML_FILTER_ENTRY_TYPES;
697
+ }
698
+ const operatorOptions = isObject(field.options) ? field.options.operators : void 0;
699
+ const operatorSchema = {
700
+ title: "Operator",
701
+ description: "The operator of the filter."
702
+ };
703
+ if (Array.isArray(operatorOptions)) {
704
+ operatorSchema.type = "string";
705
+ operatorSchema.oneOf = operatorOptions.flatMap((optionOrGroup) => {
706
+ if (isOptionGroup(optionOrGroup)) {
707
+ return optionOrGroup.options.map((option) => ({
708
+ title: `${optionOrGroup.label}: ${option.label || option.value}`,
709
+ const: option.value
710
+ }));
711
+ }
712
+ return {
713
+ title: optionOrGroup.label || String(optionOrGroup.value),
714
+ const: optionOrGroup.value
715
+ };
716
+ });
717
+ } else {
718
+ operatorSchema.enum = IML_BINARY_FILTER_OPERATORS;
719
+ }
720
+ const filterDefinition = {
387
721
  oneOf: [
388
722
  {
389
723
  type: "object",
390
724
  properties: {
391
- a: { type: IML_FILTER_ENTRY_TYPES },
392
- o: { enum: IML_UNARY_FILTER_OPERATORS }
393
- },
394
- required: ["a", "o"]
395
- },
396
- {
397
- type: "object",
398
- properties: {
399
- a: { type: IML_FILTER_ENTRY_TYPES },
400
- b: { type: IML_FILTER_ENTRY_TYPES },
401
- o: { enum: IML_BINARY_FILTER_OPERATORS }
725
+ a: operandSchema,
726
+ o: operatorSchema,
727
+ b: {
728
+ title: "Operand B",
729
+ description: "The second operand of the filter.",
730
+ type: IML_FILTER_ENTRY_TYPES
731
+ }
402
732
  },
403
733
  required: ["a", "b", "o"]
404
734
  }
405
735
  ]
406
736
  };
737
+ if (!operatorOptions) {
738
+ filterDefinition.oneOf?.push({
739
+ type: "object",
740
+ properties: {
741
+ a: operandSchema,
742
+ o: {
743
+ ...operatorSchema,
744
+ enum: IML_UNARY_FILTER_OPERATORS
745
+ }
746
+ },
747
+ required: ["a", "o"]
748
+ });
749
+ }
407
750
  const logic = field.logic ?? "default";
408
- result.items = ["and", "or"].includes(logic) ? filterItems : {
751
+ result.items = ["and", "or"].includes(logic) ? filterDefinition : {
409
752
  type: "array",
410
- items: filterItems
753
+ items: filterDefinition
411
754
  };
412
755
  Object.defineProperty(result, "x-filter", {
413
756
  configurable: true,
@@ -433,9 +776,7 @@ function handleSelectOrPathType(field, result, context) {
433
776
  }
434
777
  });
435
778
  }
436
- const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
437
- const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
438
- const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
779
+ const { nested, domain } = extractNestedAndDomain(field);
439
780
  if (typeof optionsOrGroups === "string") {
440
781
  Object.defineProperty(result, "x-fetch", {
441
782
  configurable: true,
@@ -504,46 +845,7 @@ function handleSelectOrPathType(field, result, context) {
504
845
  result.enum = (options || []).map((option) => option.value);
505
846
  }
506
847
  }
507
- if (nested && domain && domain !== context.domain) {
508
- const normalizedNested = typeof nested === "string" ? [nested] : nested;
509
- let root = context.roots[domain];
510
- if (!root) {
511
- const buffer = [];
512
- root = context.roots[domain] = {
513
- buffer,
514
- addFields: (nested2, tail) => {
515
- buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
516
- }
517
- };
518
- }
519
- root.addFields(normalizedNested, [...context.tail, field.name]);
520
- } else if (nested) {
521
- Object.defineProperty(result, "x-nested", {
522
- configurable: true,
523
- enumerable: true,
524
- writable: true,
525
- value: typeof nested === "string" ? { $ref: appendQueryString(nested, context.domain, [...context.tail, field.name]) } : nestedContainsStrings ? {
526
- type: "object",
527
- allOf: nested.map(
528
- (item) => typeof item === "string" ? { $ref: appendQueryString(item, context.domain, [...context.tail, field.name]) } : toJSONSchemaInternal(
529
- { type: "collection", spec: [item] },
530
- {
531
- ...context,
532
- domain: domain || context.domain,
533
- tail: [...context.tail, field.name]
534
- }
535
- )
536
- )
537
- } : toJSONSchemaInternal(
538
- { type: "collection", spec: nested },
539
- {
540
- ...context,
541
- domain: domain || context.domain,
542
- tail: [...context.tail, field.name]
543
- }
544
- )
545
- });
546
- }
848
+ result = handleNestedWithDomain(field, nested, domain, result, context);
547
849
  if (field.rpc) result = processRpcDirective(field, result, context);
548
850
  return result;
549
851
  }
@@ -566,6 +868,10 @@ function handlePrimitiveType(field, result, context) {
566
868
  }
567
869
  }
568
870
  if (field.rpc) result = processRpcDirective(field, result, context);
871
+ if (field.nested) {
872
+ const { nested, domain } = extractNestedAndDomain(field);
873
+ result = handleNestedWithDomain(field, nested, domain, result, context);
874
+ }
569
875
  return result;
570
876
  }
571
877
  function processRpcDirective(field, result, context) {
@@ -582,6 +888,60 @@ function processRpcDirective(field, result, context) {
582
888
  });
583
889
  return result;
584
890
  }
891
+ function extractNestedAndDomain(field) {
892
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : isObject(field.nested) ? field.nested.store : field.nested;
893
+ const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : isObject(field.nested) && field.nested.domain ? field.nested.domain : void 0;
894
+ return { nested, domain };
895
+ }
896
+ function handleNestedWithDomain(field, nested, domain, result, context) {
897
+ if (!nested) return result;
898
+ if (domain && domain !== context.domain) {
899
+ const normalizedNested = typeof nested === "string" ? [nested] : nested;
900
+ let root = context.roots[domain];
901
+ if (!root) {
902
+ const buffer = [];
903
+ root = context.roots[domain] = {
904
+ buffer,
905
+ addFields: (nested2, tail) => {
906
+ buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
907
+ }
908
+ };
909
+ }
910
+ root.addFields(normalizedNested, [...context.tail, field.name]);
911
+ return result;
912
+ }
913
+ return processNestedDirective(field, nested, domain, result, context);
914
+ }
915
+ function processNestedDirective(field, nested, domain, result, context) {
916
+ if (!nested) return result;
917
+ const nestedContainsStrings = Array.isArray(nested) && nested.some((item) => typeof item === "string");
918
+ Object.defineProperty(result, "x-nested", {
919
+ configurable: true,
920
+ enumerable: true,
921
+ writable: true,
922
+ value: typeof nested === "string" ? { $ref: appendQueryString(nested, context.domain, [...context.tail, field.name]) } : nestedContainsStrings ? {
923
+ type: "object",
924
+ allOf: nested.map(
925
+ (item) => typeof item === "string" ? { $ref: appendQueryString(item, context.domain, [...context.tail, field.name]) } : toJSONSchemaInternal(
926
+ { type: "collection", spec: [item] },
927
+ {
928
+ ...context,
929
+ domain: domain || context.domain,
930
+ tail: [...context.tail, field.name]
931
+ }
932
+ )
933
+ )
934
+ } : toJSONSchemaInternal(
935
+ { type: "collection", spec: nested },
936
+ {
937
+ ...context,
938
+ domain: domain || context.domain,
939
+ tail: [...context.tail, field.name]
940
+ }
941
+ )
942
+ });
943
+ return result;
944
+ }
585
945
 
586
946
  // src/validator.ts
587
947
  var FORMAN_TYPE_MAP2 = {
@@ -616,6 +976,8 @@ var FORMAN_TYPE_MAP2 = {
616
976
  pkey: "string",
617
977
  port: "number",
618
978
  select: void 0,
979
+ udttype: "string",
980
+ udtspec: "array",
619
981
  time: "string",
620
982
  timestamp: "string",
621
983
  timezone: "string",
@@ -756,6 +1118,12 @@ async function validateFormanValue(value, field, context) {
756
1118
  errors: []
757
1119
  };
758
1120
  }
1121
+ if (normalizedField.type === "udttype") {
1122
+ return validateFormanValue(value, udttypeExpand({ ...normalizedField }), context);
1123
+ }
1124
+ if (normalizedField.type === "udtspec") {
1125
+ return validateFormanValue(value, udtspecExpand({ ...normalizedField }), context);
1126
+ }
759
1127
  switch (normalizedField.type) {
760
1128
  case "collection":
761
1129
  return handleCollectionType2(value, normalizedField, context);
@@ -903,24 +1271,40 @@ async function handleArrayType2(value, field, context) {
903
1271
  };
904
1272
  }
905
1273
  async function handleFilterType2(value, field, context) {
1274
+ const operandOptions = isObject(field.options) ? field.options.store : field.options;
1275
+ const operandSpec = {
1276
+ name: "a",
1277
+ type: "any",
1278
+ required: true
1279
+ };
1280
+ if (Array.isArray(operandOptions)) {
1281
+ operandSpec.type = "select";
1282
+ operandSpec.options = operandOptions;
1283
+ }
1284
+ const operatorOptions = isObject(field.options) ? field.options.operators : void 0;
1285
+ const operatorSpec = {
1286
+ name: "o",
1287
+ type: "any",
1288
+ required: true
1289
+ };
1290
+ if (Array.isArray(operatorOptions)) {
1291
+ operatorSpec.type = "select";
1292
+ operatorSpec.grouped = true;
1293
+ operatorSpec.options = operatorOptions;
1294
+ } else {
1295
+ operatorSpec.type = "text";
1296
+ operatorSpec.validate = {
1297
+ enum: IML_FILTER_OPERATORS
1298
+ };
1299
+ }
906
1300
  const filterEntry = {
907
1301
  type: "collection",
908
1302
  spec: [
909
- {
910
- name: "a",
911
- type: "any",
912
- required: true
913
- },
1303
+ operandSpec,
1304
+ operatorSpec,
914
1305
  {
915
1306
  name: "b",
916
1307
  type: "any"
917
- },
918
- {
919
- name: "o",
920
- type: "text",
921
- validate: {
922
- enum: IML_FILTER_OPERATORS
923
- }
924
1308
  }
925
1309
  ]
926
1310
  };
@@ -1089,7 +1473,18 @@ async function handleSelectType(value, field, context) {
1089
1473
  let nested = field.nested ? field.nested : isObject(field.options) ? field.options.nested : void 0;
1090
1474
  if (typeof optionsOrGroups === "string") {
1091
1475
  try {
1092
- optionsOrGroups = await context.resolveRemote(optionsOrGroups, context);
1476
+ const resolved = await context.resolveRemote(optionsOrGroups, context);
1477
+ if (resolved == null || typeof resolved !== "object") {
1478
+ return {
1479
+ valid: false,
1480
+ errors: [...errors, {
1481
+ domain: context.domain,
1482
+ path: context.path.join("."),
1483
+ message: `Remote resource ${optionsOrGroups} returned no data.`
1484
+ }]
1485
+ };
1486
+ }
1487
+ optionsOrGroups = Array.isArray(resolved) ? resolved : [resolved];
1093
1488
  } catch (error) {
1094
1489
  return {
1095
1490
  valid: false,
@@ -1199,7 +1594,12 @@ async function handleNestedFields(nested, value, field, context) {
1199
1594
  for (const item of store) {
1200
1595
  if (typeof item === "string") {
1201
1596
  try {
1202
- resolvedStore.push(...await context.resolveRemote(item, context));
1597
+ const resolved = await context.resolveRemote(item, context);
1598
+ if (Array.isArray(resolved)) {
1599
+ resolvedStore.push(...resolved);
1600
+ } else if (resolved && typeof resolved === "object") {
1601
+ resolvedStore.push(resolved);
1602
+ }
1203
1603
  } catch (error) {
1204
1604
  return {
1205
1605
  valid: false,
@@ -1307,6 +1707,9 @@ var JSON_PRIMITIVE_TYPE_MAP = {
1307
1707
  boolean: "boolean"
1308
1708
  };
1309
1709
  function toFormanSchema(field) {
1710
+ const compositeType = Object.getOwnPropertyDescriptor(field, "x-composite")?.value;
1711
+ if (compositeType === "udttype") return udttypeCollapse(field);
1712
+ if (compositeType === "udtspec") return udtspecCollapse(field);
1310
1713
  switch (field.type) {
1311
1714
  case "object":
1312
1715
  if (!field.properties || !Object.entries(field.properties).length) {
@@ -1429,7 +1832,17 @@ function handleSearchDirective(formanField, directive) {
1429
1832
 
1430
1833
  // src/index.ts
1431
1834
  function toJSONSchema(field) {
1432
- return toJSONSchemaInternal(field);
1835
+ const context = createDefaultContext();
1836
+ const result = toJSONSchemaInternal(field, context);
1837
+ if (Object.keys(context.definitions ?? {}).length > 0) {
1838
+ Object.defineProperty(result, "definitions", {
1839
+ configurable: true,
1840
+ enumerable: true,
1841
+ writable: true,
1842
+ value: context.definitions
1843
+ });
1844
+ }
1845
+ return result;
1433
1846
  }
1434
1847
  function validateFormanWithDomains(domains, options) {
1435
1848
  return validateFormanWithDomainsInternal(domains, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",