@makehq/forman-schema 1.0.0 → 1.1.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
@@ -24,73 +24,307 @@ __export(index_exports, {
24
24
  toJSONSchema: () => toJSONSchema
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
- var FORMAN_PRIMITIVE_TYPE_MAP = {
27
+
28
+ // src/utils.ts
29
+ function noEmpty(text) {
30
+ return text?.trim() || void 0;
31
+ }
32
+ function isObject(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+
36
+ // src/forman.ts
37
+ var SchemaConversionError = class extends Error {
38
+ constructor(message, field) {
39
+ super(message);
40
+ this.field = field;
41
+ this.name = "SchemaConversionError";
42
+ }
43
+ };
44
+ var API_ENDPOINTS = {
45
+ CONNECTIONS: "api://connections",
46
+ HOOKS: "api://hooks",
47
+ KEYS: "api://keys"
48
+ };
49
+ var FORMAN_TYPE_MAP = {
50
+ account: "number",
51
+ hook: "number",
52
+ keychain: "number",
53
+ datastore: "number",
54
+ aiagent: "string",
55
+ array: "array",
56
+ collection: "object",
28
57
  text: "string",
29
58
  number: "number",
30
59
  boolean: "boolean",
31
60
  date: "string",
32
- json: "string"
61
+ json: "string",
62
+ buffer: "string",
63
+ cert: "string",
64
+ color: "string",
65
+ email: "string",
66
+ filename: "string",
67
+ file: "string",
68
+ folder: "string",
69
+ hidden: "string",
70
+ integer: "number",
71
+ uinteger: "number",
72
+ password: "string",
73
+ path: "string",
74
+ pkey: "string",
75
+ port: "number",
76
+ select: "string",
77
+ time: "string",
78
+ timestamp: "string",
79
+ timezone: "string",
80
+ url: "string",
81
+ uuid: "string"
33
82
  };
34
- var JSON_PRIMITIVE_TYPE_MAP = {
35
- string: "text",
36
- number: "number",
37
- boolean: "boolean"
38
- };
39
- function noEmpty(text) {
40
- if (!text) return void 0;
41
- return text;
83
+ function validateFormanField(field) {
84
+ if (!field.type) {
85
+ throw new SchemaConversionError("Field type is required", field);
86
+ }
87
+ const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
88
+ if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
89
+ throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
90
+ }
42
91
  }
43
- function toJSONSchema(field) {
44
- switch (field.type) {
92
+ function normalizeFieldType(field) {
93
+ const typeHandlers = {
94
+ "account:": (type) => ({
95
+ ...field,
96
+ type: "account",
97
+ options: {
98
+ ...field.options,
99
+ store: `${API_ENDPOINTS.CONNECTIONS}/${type.substring(8)}`
100
+ }
101
+ }),
102
+ "hook:": (type) => ({
103
+ ...field,
104
+ type: "hook",
105
+ options: {
106
+ ...field.options,
107
+ store: `${API_ENDPOINTS.HOOKS}/${type.substring(5)}`
108
+ }
109
+ }),
110
+ "keychain:": (type) => ({
111
+ ...field,
112
+ type: "keychain",
113
+ options: {
114
+ ...field.options,
115
+ store: `${API_ENDPOINTS.KEYS}/${type.substring(9)}`
116
+ }
117
+ })
118
+ };
119
+ for (const [prefix, handler] of Object.entries(typeHandlers)) {
120
+ if (field.type.startsWith(prefix)) {
121
+ return handler(field.type);
122
+ }
123
+ }
124
+ return field;
125
+ }
126
+ function appendQueryString(path, domain, tail) {
127
+ if (path.startsWith("api://")) return path;
128
+ const queryString = tail.map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
129
+ if (!queryString) return path;
130
+ const separator = path.includes("?") ? "&" : "?";
131
+ return `${path}${separator}${queryString}`;
132
+ }
133
+ function createDefaultContext() {
134
+ return {
135
+ domain: "default",
136
+ tail: [],
137
+ path: [],
138
+ roots: {},
139
+ addConditionalFields: () => {
140
+ throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
141
+ }
142
+ };
143
+ }
144
+ function toJSONSchemaInternal(field, context = createDefaultContext()) {
145
+ validateFormanField(field);
146
+ const normalizedField = normalizeFieldType(field);
147
+ const result = {
148
+ type: FORMAN_TYPE_MAP[normalizedField.type] || "string",
149
+ title: noEmpty(normalizedField.label),
150
+ description: noEmpty(normalizedField.help)
151
+ };
152
+ switch (normalizedField.type) {
45
153
  case "collection":
46
- const required = [];
47
- const properties = (Array.isArray(field.spec) ? field.spec : []).reduce(
48
- (object, subField) => {
49
- if (!subField.name) return object;
50
- if (subField.required) required.push(subField.name);
51
- return Object.defineProperty(object, subField.name, {
52
- enumerable: true,
53
- value: toJSONSchema(subField)
54
- });
55
- },
56
- {}
57
- );
58
- return {
59
- type: "object",
60
- description: noEmpty(field.help),
61
- properties,
62
- required
63
- };
154
+ return handleCollectionType(normalizedField, result, context);
64
155
  case "array":
65
- return {
66
- type: "array",
67
- description: noEmpty(field.help),
68
- items: field.spec && toJSONSchema(
69
- Array.isArray(field.spec) ? {
70
- // If spec is an array, treat it as a collection
71
- type: "collection",
72
- spec: field.spec
73
- } : field.spec
74
- )
75
- };
156
+ return handleArrayType(normalizedField, result, context);
76
157
  case "select":
77
- return {
78
- type: "string",
79
- description: noEmpty(field.help),
80
- enum: (field.options || []).map((option) => option.value)
81
- };
158
+ case "account":
159
+ case "hook":
160
+ case "keychain":
161
+ case "datastore":
162
+ case "aiagent":
163
+ case "file":
164
+ return handleSelectType(normalizedField, result, context);
82
165
  default:
166
+ return handlePrimitiveType(normalizedField, result);
167
+ }
168
+ }
169
+ function handleCollectionType(field, result, context) {
170
+ Object.assign(result, {
171
+ type: "object",
172
+ properties: {},
173
+ required: []
174
+ });
175
+ function addField(subField, tail) {
176
+ if (!subField.name) return;
177
+ if (subField.required) {
178
+ result.required.push(subField.name);
179
+ }
180
+ Object.defineProperty(result.properties, subField.name, {
181
+ enumerable: true,
182
+ value: toJSONSchemaInternal(subField, {
183
+ ...context,
184
+ domain: field["x-domain-root"] || context.domain,
185
+ tail: tail || context.tail,
186
+ path: [...context.path, field.name],
187
+ addConditionalFields: (name, value, nested) => {
188
+ result.allOf ||= [];
189
+ result.allOf.push({
190
+ if: {
191
+ properties: {
192
+ [name]: { const: value }
193
+ }
194
+ },
195
+ then: typeof nested === "string" ? { $ref: `${nested}#` } : nested
196
+ });
197
+ }
198
+ })
199
+ });
200
+ }
201
+ if (field["x-domain-root"]) {
202
+ const domainRoot = field["x-domain-root"];
203
+ const buffer = context.roots[domainRoot]?.buffer;
204
+ context.roots[domainRoot] = {
205
+ addFields: (nested, tail) => {
206
+ nested.forEach((subField) => addField(subField, tail));
207
+ }
208
+ };
209
+ if (buffer) {
210
+ buffer.forEach((item) => addField(item.field, item.tail));
211
+ }
212
+ }
213
+ if (Array.isArray(field.spec)) {
214
+ field.spec.forEach((subField) => addField(subField));
215
+ }
216
+ return result;
217
+ }
218
+ function handleArrayType(field, result, context) {
219
+ if (field.spec) {
220
+ result.items = toJSONSchemaInternal(
221
+ Array.isArray(field.spec) ? { type: "collection", spec: field.spec } : field.spec,
222
+ {
223
+ ...context,
224
+ path: [...context.path, `${field.name}[]`]
225
+ }
226
+ );
227
+ }
228
+ if (field.validate) {
229
+ if (field.validate.minItems !== void 0) {
230
+ result.minItems = field.validate.minItems;
231
+ }
232
+ if (field.validate.maxItems !== void 0) {
233
+ result.maxItems = field.validate.maxItems;
234
+ }
235
+ }
236
+ return result;
237
+ }
238
+ function handleSelectType(field, result, context) {
239
+ const options = isObject(field.options) ? field.options.store : field.options;
240
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
241
+ const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
242
+ if (typeof options === "string") {
243
+ result["x-fetch"] = appendQueryString(options, context.domain, context.tail);
244
+ } else if (options?.some((option) => option.label || option.nested)) {
245
+ result.oneOf = (options || []).map((option) => {
246
+ const localNested = (isObject(option.nested) ? option.nested.store : option.nested) || nested;
247
+ const localDomain = (isObject(option.nested) && option.nested.domain ? option.nested.domain : domain) || context.domain;
248
+ if (localNested) {
249
+ context.addConditionalFields(
250
+ field.name,
251
+ option.value,
252
+ typeof localNested === "string" ? appendQueryString(localNested, localDomain, [...context.tail, field.name]) : toJSONSchemaInternal(
253
+ { type: "collection", spec: localNested },
254
+ {
255
+ ...context,
256
+ domain: localDomain,
257
+ tail: [...context.tail, field.name]
258
+ }
259
+ )
260
+ );
261
+ }
83
262
  return {
84
- type: FORMAN_PRIMITIVE_TYPE_MAP[field.type],
85
- default: field.default != "" && field.default != null ? field.default : void 0,
86
- description: noEmpty(field.help)
263
+ title: noEmpty(option.label),
264
+ const: option.value
87
265
  };
266
+ });
267
+ } else {
268
+ result.enum = (options || []).map((option) => option.value);
88
269
  }
270
+ if (nested && domain && domain !== context.domain) {
271
+ if (typeof nested === "string") {
272
+ throw new SchemaConversionError("Dynamic nested fields with domain change are not supported.");
273
+ }
274
+ let root = context.roots[domain];
275
+ if (!root) {
276
+ const buffer = [];
277
+ root = context.roots[domain] = {
278
+ buffer,
279
+ addFields: (nested2, tail) => {
280
+ buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
281
+ }
282
+ };
283
+ }
284
+ root.addFields(nested, [...context.tail, field.name]);
285
+ } else if (nested) {
286
+ result["x-nested"] = typeof nested === "string" ? nested : toJSONSchemaInternal(
287
+ { type: "collection", spec: nested },
288
+ {
289
+ ...context,
290
+ domain: domain || context.domain,
291
+ tail: [...context.tail, field.name]
292
+ }
293
+ );
294
+ }
295
+ return result;
296
+ }
297
+ function handlePrimitiveType(field, result) {
298
+ if (field.default !== "" && field.default != null) {
299
+ result.default = field.default;
300
+ }
301
+ if (field.validate) {
302
+ if (field.validate.pattern) {
303
+ result.pattern = field.validate.pattern;
304
+ }
305
+ if (field.validate.min !== void 0) {
306
+ result.minimum = field.validate.min;
307
+ }
308
+ if (field.validate.max !== void 0) {
309
+ result.maximum = field.validate.max;
310
+ }
311
+ if (field.validate.enum) {
312
+ result.enum = field.validate.enum;
313
+ }
314
+ }
315
+ return result;
89
316
  }
317
+
318
+ // src/json.ts
319
+ var JSON_PRIMITIVE_TYPE_MAP = {
320
+ string: "text",
321
+ number: "number",
322
+ boolean: "boolean"
323
+ };
90
324
  function toFormanSchema(field) {
91
325
  switch (field.type) {
92
326
  case "object":
93
- const spec = field.properties ? Object.entries(field.properties).map(([name, property]) => {
327
+ const spec = field.properties ? Object.entries(field.properties).filter(([name, property]) => !!property).map(([name, property]) => {
94
328
  const subField = toFormanSchema(property);
95
329
  subField.name = name;
96
330
  subField.required = field.required?.includes(name) || false;
@@ -98,41 +332,76 @@ function toFormanSchema(field) {
98
332
  }) : [];
99
333
  return {
100
334
  type: "collection",
101
- help: field.description,
335
+ label: noEmpty(field.title),
336
+ help: noEmpty(field.description),
102
337
  spec
103
338
  };
104
339
  case "array":
105
- return {
340
+ const items = field.items && isObject(field.items) ? field.items : void 0;
341
+ const formanSchema = {
106
342
  type: "array",
107
- help: field.description,
108
- spec: field.items && (field.items?.type === "object" && field.items.properties ? Object.entries(field.items.properties).map(([name, property]) => {
343
+ label: noEmpty(field.title),
344
+ help: noEmpty(field.description),
345
+ spec: items ? items.type === "object" && items.properties ? Object.entries(items.properties).map(([name, property]) => {
109
346
  const subField = toFormanSchema(property);
110
347
  subField.name = name;
111
- subField.required = field.items?.required?.includes(name) || false;
348
+ subField.required = items.required?.includes(name) || false;
112
349
  return subField;
113
- }) : toFormanSchema(field.items))
350
+ }) : toFormanSchema(items) : void 0
114
351
  };
352
+ if (field.minItems !== void 0 || field.maxItems !== void 0) {
353
+ formanSchema.validate = formanSchema.validate || {};
354
+ if (field.minItems !== void 0) formanSchema.validate.minItems = field.minItems;
355
+ if (field.maxItems !== void 0) formanSchema.validate.maxItems = field.maxItems;
356
+ }
357
+ return formanSchema;
115
358
  case "string":
116
359
  if (field.enum) {
117
360
  return {
118
361
  type: "select",
119
- help: field.description,
362
+ label: noEmpty(field.title),
363
+ help: noEmpty(field.description),
120
364
  options: field.enum.map((value) => ({ value }))
121
365
  };
366
+ } else if (field.oneOf) {
367
+ return {
368
+ type: "select",
369
+ label: noEmpty(field.title),
370
+ help: noEmpty(field.description),
371
+ options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
372
+ };
122
373
  }
123
- return {
374
+ const textField = {
124
375
  type: "text",
125
- help: field.description,
376
+ label: noEmpty(field.title),
377
+ help: noEmpty(field.description),
126
378
  default: field.default
127
379
  };
380
+ if (field.pattern || field.enum) {
381
+ textField.validate = textField.validate || {};
382
+ if (field.pattern) textField.validate.pattern = field.pattern;
383
+ if (field.enum) textField.validate.enum = field.enum;
384
+ }
385
+ return textField;
128
386
  default:
129
- return {
130
- type: JSON_PRIMITIVE_TYPE_MAP[field.type],
387
+ const primitiveField = {
388
+ type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "text",
131
389
  help: field.description,
132
390
  default: field.default
133
391
  };
392
+ if (field.minimum !== void 0 || field.maximum !== void 0) {
393
+ primitiveField.validate = primitiveField.validate || {};
394
+ if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
395
+ if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
396
+ }
397
+ return primitiveField;
134
398
  }
135
399
  }
400
+
401
+ // src/index.ts
402
+ function toJSONSchema(field) {
403
+ return toJSONSchemaInternal(field);
404
+ }
136
405
  // Annotate the CommonJS export names for ESM import in node:
137
406
  0 && (module.exports = {
138
407
  toFormanSchema,
package/dist/index.d.cts CHANGED
@@ -1,3 +1,27 @@
1
+ import { JSONSchema7 } from 'json-schema';
2
+ export { JSONSchema7 } from 'json-schema';
3
+
4
+ /**
5
+ * Valid Forman Schema field types
6
+ */
7
+ type FormanSchemaFieldType = 'account' | 'hook' | 'keychain' | 'datastore' | 'aiagent' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | string;
8
+ /**
9
+ * Validation configuration for Forman Schema fields
10
+ */
11
+ interface FormanSchemaValidation {
12
+ /** Pattern for string validation */
13
+ pattern?: string;
14
+ /** Minimum value */
15
+ min?: number;
16
+ /** Maximum value */
17
+ max?: number;
18
+ /** Minimum number of items */
19
+ minItems?: number;
20
+ /** Maximum number of items */
21
+ maxItems?: number;
22
+ /** Enumeration of allowed values */
23
+ enum?: string[];
24
+ }
1
25
  /**
2
26
  * Represents a field in Forman Schema format.
3
27
  */
@@ -5,51 +29,59 @@ type FormanSchemaField = {
5
29
  /** Field name identifier */
6
30
  name?: string;
7
31
  /** The field type (e.g., 'text', 'number', 'boolean', 'collection', 'array', etc.) */
8
- type: string;
32
+ type: FormanSchemaFieldType;
9
33
  /** Whether the field is required or not */
10
34
  required?: boolean;
11
35
  /** Default value for the field */
12
- default?: string | number | boolean | null;
36
+ default?: FormanSchemaValue;
13
37
  /** Available options for select type fields */
14
- options?: {
15
- /** Option value */
16
- value: string;
17
- }[];
38
+ options?: FormanSchemaOption[] | FormanSchemaExtendedOptions | string;
18
39
  /** Help text or description for the field */
19
40
  help?: string;
20
41
  /** Sub-fields specification for collection or array types */
21
42
  spec?: FormanSchemaField[] | FormanSchemaField;
43
+ /** Hide field behind advanced toggle */
44
+ advanced?: boolean;
45
+ /** Human readable label for the field */
46
+ label?: string;
47
+ /** Nested fields */
48
+ nested?: FormanSchemaNested;
49
+ /** Validation rules */
50
+ validate?: FormanSchemaValidation;
51
+ } & Record<`x-${string}`, unknown>;
52
+ type FormanSchemaValue = string | number | boolean | null;
53
+ type FormanSchemaOption = {
54
+ /** Option value */
55
+ value: FormanSchemaValue;
56
+ /** Option label */
57
+ label?: string;
58
+ /** Nested fields for this option */
59
+ nested?: FormanSchemaNested;
60
+ };
61
+ type FormanSchemaExtendedOptions = {
62
+ /** Store for the options */
63
+ store: FormanSchemaOption[] | string;
64
+ /** Nested fields for every option */
65
+ nested?: FormanSchemaNested;
66
+ };
67
+ type FormanSchemaNested = FormanSchemaField[] | string | FormanSchemaExtendedNested;
68
+ type FormanSchemaExtendedNested = {
69
+ store: FormanSchemaField[] | string;
70
+ domain?: string;
22
71
  };
72
+
23
73
  /**
24
- * Represents a field in JSON Schema format.
74
+ * Converts a JSON Schema field to its Forman Schema equivalent.
75
+ * @param field The JSON Schema field to convert
76
+ * @returns The equivalent Forman Schema field
25
77
  */
26
- type JSONSchemaField = {
27
- /** The JSON Schema type (e.g., 'string', 'number', 'boolean', 'object', 'array', etc.) */
28
- type: string;
29
- /** Field description */
30
- description?: string;
31
- /** Default value for the field */
32
- default?: string | number | boolean | null;
33
- /** Enumeration of allowed values for the field */
34
- enum?: string[];
35
- /** Child properties for object type */
36
- properties?: Record<string, JSONSchemaField>;
37
- /** Array item specification for array type */
38
- items?: JSONSchemaField;
39
- /** List of required property names */
40
- required?: string[];
41
- };
78
+ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
79
+
42
80
  /**
43
81
  * Converts a Forman Schema field to its JSON Schema equivalent.
44
- * @param field - The Forman Schema field to convert
82
+ * @param field The Forman Schema field to convert
45
83
  * @returns The equivalent JSON Schema field
46
84
  */
47
- declare function toJSONSchema(field: FormanSchemaField): JSONSchemaField;
48
- /**
49
- * Converts a JSON Schema field to its Forman Schema equivalent.
50
- * @param field - The JSON Schema field to convert
51
- * @returns The equivalent Forman Schema field
52
- */
53
- declare function toFormanSchema(field: JSONSchemaField): FormanSchemaField;
85
+ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
54
86
 
55
- export { type FormanSchemaField, type JSONSchemaField, toFormanSchema, toJSONSchema };
87
+ export { type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaValue, toFormanSchema, toJSONSchema };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,27 @@
1
+ import { JSONSchema7 } from 'json-schema';
2
+ export { JSONSchema7 } from 'json-schema';
3
+
4
+ /**
5
+ * Valid Forman Schema field types
6
+ */
7
+ type FormanSchemaFieldType = 'account' | 'hook' | 'keychain' | 'datastore' | 'aiagent' | 'array' | 'collection' | 'text' | 'number' | 'boolean' | 'date' | 'json' | 'buffer' | 'cert' | 'color' | 'email' | 'filename' | 'file' | 'folder' | 'hidden' | 'integer' | 'uinteger' | 'path' | 'pkey' | 'port' | 'select' | 'time' | 'timestamp' | 'timezone' | 'url' | 'uuid' | `account:${string}` | `hook:${string}` | `keychain:${string}` | string;
8
+ /**
9
+ * Validation configuration for Forman Schema fields
10
+ */
11
+ interface FormanSchemaValidation {
12
+ /** Pattern for string validation */
13
+ pattern?: string;
14
+ /** Minimum value */
15
+ min?: number;
16
+ /** Maximum value */
17
+ max?: number;
18
+ /** Minimum number of items */
19
+ minItems?: number;
20
+ /** Maximum number of items */
21
+ maxItems?: number;
22
+ /** Enumeration of allowed values */
23
+ enum?: string[];
24
+ }
1
25
  /**
2
26
  * Represents a field in Forman Schema format.
3
27
  */
@@ -5,51 +29,59 @@ type FormanSchemaField = {
5
29
  /** Field name identifier */
6
30
  name?: string;
7
31
  /** The field type (e.g., 'text', 'number', 'boolean', 'collection', 'array', etc.) */
8
- type: string;
32
+ type: FormanSchemaFieldType;
9
33
  /** Whether the field is required or not */
10
34
  required?: boolean;
11
35
  /** Default value for the field */
12
- default?: string | number | boolean | null;
36
+ default?: FormanSchemaValue;
13
37
  /** Available options for select type fields */
14
- options?: {
15
- /** Option value */
16
- value: string;
17
- }[];
38
+ options?: FormanSchemaOption[] | FormanSchemaExtendedOptions | string;
18
39
  /** Help text or description for the field */
19
40
  help?: string;
20
41
  /** Sub-fields specification for collection or array types */
21
42
  spec?: FormanSchemaField[] | FormanSchemaField;
43
+ /** Hide field behind advanced toggle */
44
+ advanced?: boolean;
45
+ /** Human readable label for the field */
46
+ label?: string;
47
+ /** Nested fields */
48
+ nested?: FormanSchemaNested;
49
+ /** Validation rules */
50
+ validate?: FormanSchemaValidation;
51
+ } & Record<`x-${string}`, unknown>;
52
+ type FormanSchemaValue = string | number | boolean | null;
53
+ type FormanSchemaOption = {
54
+ /** Option value */
55
+ value: FormanSchemaValue;
56
+ /** Option label */
57
+ label?: string;
58
+ /** Nested fields for this option */
59
+ nested?: FormanSchemaNested;
60
+ };
61
+ type FormanSchemaExtendedOptions = {
62
+ /** Store for the options */
63
+ store: FormanSchemaOption[] | string;
64
+ /** Nested fields for every option */
65
+ nested?: FormanSchemaNested;
66
+ };
67
+ type FormanSchemaNested = FormanSchemaField[] | string | FormanSchemaExtendedNested;
68
+ type FormanSchemaExtendedNested = {
69
+ store: FormanSchemaField[] | string;
70
+ domain?: string;
22
71
  };
72
+
23
73
  /**
24
- * Represents a field in JSON Schema format.
74
+ * Converts a JSON Schema field to its Forman Schema equivalent.
75
+ * @param field The JSON Schema field to convert
76
+ * @returns The equivalent Forman Schema field
25
77
  */
26
- type JSONSchemaField = {
27
- /** The JSON Schema type (e.g., 'string', 'number', 'boolean', 'object', 'array', etc.) */
28
- type: string;
29
- /** Field description */
30
- description?: string;
31
- /** Default value for the field */
32
- default?: string | number | boolean | null;
33
- /** Enumeration of allowed values for the field */
34
- enum?: string[];
35
- /** Child properties for object type */
36
- properties?: Record<string, JSONSchemaField>;
37
- /** Array item specification for array type */
38
- items?: JSONSchemaField;
39
- /** List of required property names */
40
- required?: string[];
41
- };
78
+ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
79
+
42
80
  /**
43
81
  * Converts a Forman Schema field to its JSON Schema equivalent.
44
- * @param field - The Forman Schema field to convert
82
+ * @param field The Forman Schema field to convert
45
83
  * @returns The equivalent JSON Schema field
46
84
  */
47
- declare function toJSONSchema(field: FormanSchemaField): JSONSchemaField;
48
- /**
49
- * Converts a JSON Schema field to its Forman Schema equivalent.
50
- * @param field - The JSON Schema field to convert
51
- * @returns The equivalent Forman Schema field
52
- */
53
- declare function toFormanSchema(field: JSONSchemaField): FormanSchemaField;
85
+ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
54
86
 
55
- export { type FormanSchemaField, type JSONSchemaField, toFormanSchema, toJSONSchema };
87
+ export { type FormanSchemaExtendedNested, type FormanSchemaExtendedOptions, type FormanSchemaField, type FormanSchemaFieldType, type FormanSchemaNested, type FormanSchemaOption, type FormanSchemaValue, toFormanSchema, toJSONSchema };
package/dist/index.js CHANGED
@@ -1,71 +1,303 @@
1
- // src/index.ts
2
- var FORMAN_PRIMITIVE_TYPE_MAP = {
1
+ // src/utils.ts
2
+ function noEmpty(text) {
3
+ return text?.trim() || void 0;
4
+ }
5
+ function isObject(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ // src/forman.ts
10
+ var SchemaConversionError = class extends Error {
11
+ constructor(message, field) {
12
+ super(message);
13
+ this.field = field;
14
+ this.name = "SchemaConversionError";
15
+ }
16
+ };
17
+ var API_ENDPOINTS = {
18
+ CONNECTIONS: "api://connections",
19
+ HOOKS: "api://hooks",
20
+ KEYS: "api://keys"
21
+ };
22
+ var FORMAN_TYPE_MAP = {
23
+ account: "number",
24
+ hook: "number",
25
+ keychain: "number",
26
+ datastore: "number",
27
+ aiagent: "string",
28
+ array: "array",
29
+ collection: "object",
3
30
  text: "string",
4
31
  number: "number",
5
32
  boolean: "boolean",
6
33
  date: "string",
7
- json: "string"
8
- };
9
- var JSON_PRIMITIVE_TYPE_MAP = {
10
- string: "text",
11
- number: "number",
12
- boolean: "boolean"
34
+ json: "string",
35
+ buffer: "string",
36
+ cert: "string",
37
+ color: "string",
38
+ email: "string",
39
+ filename: "string",
40
+ file: "string",
41
+ folder: "string",
42
+ hidden: "string",
43
+ integer: "number",
44
+ uinteger: "number",
45
+ password: "string",
46
+ path: "string",
47
+ pkey: "string",
48
+ port: "number",
49
+ select: "string",
50
+ time: "string",
51
+ timestamp: "string",
52
+ timezone: "string",
53
+ url: "string",
54
+ uuid: "string"
13
55
  };
14
- function noEmpty(text) {
15
- if (!text) return void 0;
16
- return text;
56
+ function validateFormanField(field) {
57
+ if (!field.type) {
58
+ throw new SchemaConversionError("Field type is required", field);
59
+ }
60
+ const normalizedType = field.type.includes(":") ? field.type.split(":")[0] : field.type;
61
+ if (!Object.keys(FORMAN_TYPE_MAP).includes(normalizedType)) {
62
+ throw new SchemaConversionError(`Unknown field type: ${field.type}`, field);
63
+ }
17
64
  }
18
- function toJSONSchema(field) {
19
- switch (field.type) {
65
+ function normalizeFieldType(field) {
66
+ const typeHandlers = {
67
+ "account:": (type) => ({
68
+ ...field,
69
+ type: "account",
70
+ options: {
71
+ ...field.options,
72
+ store: `${API_ENDPOINTS.CONNECTIONS}/${type.substring(8)}`
73
+ }
74
+ }),
75
+ "hook:": (type) => ({
76
+ ...field,
77
+ type: "hook",
78
+ options: {
79
+ ...field.options,
80
+ store: `${API_ENDPOINTS.HOOKS}/${type.substring(5)}`
81
+ }
82
+ }),
83
+ "keychain:": (type) => ({
84
+ ...field,
85
+ type: "keychain",
86
+ options: {
87
+ ...field.options,
88
+ store: `${API_ENDPOINTS.KEYS}/${type.substring(9)}`
89
+ }
90
+ })
91
+ };
92
+ for (const [prefix, handler] of Object.entries(typeHandlers)) {
93
+ if (field.type.startsWith(prefix)) {
94
+ return handler(field.type);
95
+ }
96
+ }
97
+ return field;
98
+ }
99
+ function appendQueryString(path, domain, tail) {
100
+ if (path.startsWith("api://")) return path;
101
+ const queryString = tail.map((part) => `${encodeURIComponent(part)}={{${part}}}`).join("&");
102
+ if (!queryString) return path;
103
+ const separator = path.includes("?") ? "&" : "?";
104
+ return `${path}${separator}${queryString}`;
105
+ }
106
+ function createDefaultContext() {
107
+ return {
108
+ domain: "default",
109
+ tail: [],
110
+ path: [],
111
+ roots: {},
112
+ addConditionalFields: () => {
113
+ throw new SchemaConversionError("Cannot serialize nested fields without parent field.");
114
+ }
115
+ };
116
+ }
117
+ function toJSONSchemaInternal(field, context = createDefaultContext()) {
118
+ validateFormanField(field);
119
+ const normalizedField = normalizeFieldType(field);
120
+ const result = {
121
+ type: FORMAN_TYPE_MAP[normalizedField.type] || "string",
122
+ title: noEmpty(normalizedField.label),
123
+ description: noEmpty(normalizedField.help)
124
+ };
125
+ switch (normalizedField.type) {
20
126
  case "collection":
21
- const required = [];
22
- const properties = (Array.isArray(field.spec) ? field.spec : []).reduce(
23
- (object, subField) => {
24
- if (!subField.name) return object;
25
- if (subField.required) required.push(subField.name);
26
- return Object.defineProperty(object, subField.name, {
27
- enumerable: true,
28
- value: toJSONSchema(subField)
29
- });
30
- },
31
- {}
32
- );
33
- return {
34
- type: "object",
35
- description: noEmpty(field.help),
36
- properties,
37
- required
38
- };
127
+ return handleCollectionType(normalizedField, result, context);
39
128
  case "array":
40
- return {
41
- type: "array",
42
- description: noEmpty(field.help),
43
- items: field.spec && toJSONSchema(
44
- Array.isArray(field.spec) ? {
45
- // If spec is an array, treat it as a collection
46
- type: "collection",
47
- spec: field.spec
48
- } : field.spec
49
- )
50
- };
129
+ return handleArrayType(normalizedField, result, context);
51
130
  case "select":
52
- return {
53
- type: "string",
54
- description: noEmpty(field.help),
55
- enum: (field.options || []).map((option) => option.value)
56
- };
131
+ case "account":
132
+ case "hook":
133
+ case "keychain":
134
+ case "datastore":
135
+ case "aiagent":
136
+ case "file":
137
+ return handleSelectType(normalizedField, result, context);
57
138
  default:
139
+ return handlePrimitiveType(normalizedField, result);
140
+ }
141
+ }
142
+ function handleCollectionType(field, result, context) {
143
+ Object.assign(result, {
144
+ type: "object",
145
+ properties: {},
146
+ required: []
147
+ });
148
+ function addField(subField, tail) {
149
+ if (!subField.name) return;
150
+ if (subField.required) {
151
+ result.required.push(subField.name);
152
+ }
153
+ Object.defineProperty(result.properties, subField.name, {
154
+ enumerable: true,
155
+ value: toJSONSchemaInternal(subField, {
156
+ ...context,
157
+ domain: field["x-domain-root"] || context.domain,
158
+ tail: tail || context.tail,
159
+ path: [...context.path, field.name],
160
+ addConditionalFields: (name, value, nested) => {
161
+ result.allOf ||= [];
162
+ result.allOf.push({
163
+ if: {
164
+ properties: {
165
+ [name]: { const: value }
166
+ }
167
+ },
168
+ then: typeof nested === "string" ? { $ref: `${nested}#` } : nested
169
+ });
170
+ }
171
+ })
172
+ });
173
+ }
174
+ if (field["x-domain-root"]) {
175
+ const domainRoot = field["x-domain-root"];
176
+ const buffer = context.roots[domainRoot]?.buffer;
177
+ context.roots[domainRoot] = {
178
+ addFields: (nested, tail) => {
179
+ nested.forEach((subField) => addField(subField, tail));
180
+ }
181
+ };
182
+ if (buffer) {
183
+ buffer.forEach((item) => addField(item.field, item.tail));
184
+ }
185
+ }
186
+ if (Array.isArray(field.spec)) {
187
+ field.spec.forEach((subField) => addField(subField));
188
+ }
189
+ return result;
190
+ }
191
+ function handleArrayType(field, result, context) {
192
+ if (field.spec) {
193
+ result.items = toJSONSchemaInternal(
194
+ Array.isArray(field.spec) ? { type: "collection", spec: field.spec } : field.spec,
195
+ {
196
+ ...context,
197
+ path: [...context.path, `${field.name}[]`]
198
+ }
199
+ );
200
+ }
201
+ if (field.validate) {
202
+ if (field.validate.minItems !== void 0) {
203
+ result.minItems = field.validate.minItems;
204
+ }
205
+ if (field.validate.maxItems !== void 0) {
206
+ result.maxItems = field.validate.maxItems;
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ function handleSelectType(field, result, context) {
212
+ const options = isObject(field.options) ? field.options.store : field.options;
213
+ const nested = isObject(field.options) ? isObject(field.options.nested) ? field.options.nested.store : field.options.nested : void 0;
214
+ const domain = isObject(field.options) ? isObject(field.options.nested) && field.options.nested.domain ? field.options.nested.domain : void 0 : void 0;
215
+ if (typeof options === "string") {
216
+ result["x-fetch"] = appendQueryString(options, context.domain, context.tail);
217
+ } else if (options?.some((option) => option.label || option.nested)) {
218
+ result.oneOf = (options || []).map((option) => {
219
+ const localNested = (isObject(option.nested) ? option.nested.store : option.nested) || nested;
220
+ const localDomain = (isObject(option.nested) && option.nested.domain ? option.nested.domain : domain) || context.domain;
221
+ if (localNested) {
222
+ context.addConditionalFields(
223
+ field.name,
224
+ option.value,
225
+ typeof localNested === "string" ? appendQueryString(localNested, localDomain, [...context.tail, field.name]) : toJSONSchemaInternal(
226
+ { type: "collection", spec: localNested },
227
+ {
228
+ ...context,
229
+ domain: localDomain,
230
+ tail: [...context.tail, field.name]
231
+ }
232
+ )
233
+ );
234
+ }
58
235
  return {
59
- type: FORMAN_PRIMITIVE_TYPE_MAP[field.type],
60
- default: field.default != "" && field.default != null ? field.default : void 0,
61
- description: noEmpty(field.help)
236
+ title: noEmpty(option.label),
237
+ const: option.value
62
238
  };
239
+ });
240
+ } else {
241
+ result.enum = (options || []).map((option) => option.value);
63
242
  }
243
+ if (nested && domain && domain !== context.domain) {
244
+ if (typeof nested === "string") {
245
+ throw new SchemaConversionError("Dynamic nested fields with domain change are not supported.");
246
+ }
247
+ let root = context.roots[domain];
248
+ if (!root) {
249
+ const buffer = [];
250
+ root = context.roots[domain] = {
251
+ buffer,
252
+ addFields: (nested2, tail) => {
253
+ buffer.push(...nested2.map((field2) => ({ field: field2, tail })));
254
+ }
255
+ };
256
+ }
257
+ root.addFields(nested, [...context.tail, field.name]);
258
+ } else if (nested) {
259
+ result["x-nested"] = typeof nested === "string" ? nested : toJSONSchemaInternal(
260
+ { type: "collection", spec: nested },
261
+ {
262
+ ...context,
263
+ domain: domain || context.domain,
264
+ tail: [...context.tail, field.name]
265
+ }
266
+ );
267
+ }
268
+ return result;
64
269
  }
270
+ function handlePrimitiveType(field, result) {
271
+ if (field.default !== "" && field.default != null) {
272
+ result.default = field.default;
273
+ }
274
+ if (field.validate) {
275
+ if (field.validate.pattern) {
276
+ result.pattern = field.validate.pattern;
277
+ }
278
+ if (field.validate.min !== void 0) {
279
+ result.minimum = field.validate.min;
280
+ }
281
+ if (field.validate.max !== void 0) {
282
+ result.maximum = field.validate.max;
283
+ }
284
+ if (field.validate.enum) {
285
+ result.enum = field.validate.enum;
286
+ }
287
+ }
288
+ return result;
289
+ }
290
+
291
+ // src/json.ts
292
+ var JSON_PRIMITIVE_TYPE_MAP = {
293
+ string: "text",
294
+ number: "number",
295
+ boolean: "boolean"
296
+ };
65
297
  function toFormanSchema(field) {
66
298
  switch (field.type) {
67
299
  case "object":
68
- const spec = field.properties ? Object.entries(field.properties).map(([name, property]) => {
300
+ const spec = field.properties ? Object.entries(field.properties).filter(([name, property]) => !!property).map(([name, property]) => {
69
301
  const subField = toFormanSchema(property);
70
302
  subField.name = name;
71
303
  subField.required = field.required?.includes(name) || false;
@@ -73,41 +305,76 @@ function toFormanSchema(field) {
73
305
  }) : [];
74
306
  return {
75
307
  type: "collection",
76
- help: field.description,
308
+ label: noEmpty(field.title),
309
+ help: noEmpty(field.description),
77
310
  spec
78
311
  };
79
312
  case "array":
80
- return {
313
+ const items = field.items && isObject(field.items) ? field.items : void 0;
314
+ const formanSchema = {
81
315
  type: "array",
82
- help: field.description,
83
- spec: field.items && (field.items?.type === "object" && field.items.properties ? Object.entries(field.items.properties).map(([name, property]) => {
316
+ label: noEmpty(field.title),
317
+ help: noEmpty(field.description),
318
+ spec: items ? items.type === "object" && items.properties ? Object.entries(items.properties).map(([name, property]) => {
84
319
  const subField = toFormanSchema(property);
85
320
  subField.name = name;
86
- subField.required = field.items?.required?.includes(name) || false;
321
+ subField.required = items.required?.includes(name) || false;
87
322
  return subField;
88
- }) : toFormanSchema(field.items))
323
+ }) : toFormanSchema(items) : void 0
89
324
  };
325
+ if (field.minItems !== void 0 || field.maxItems !== void 0) {
326
+ formanSchema.validate = formanSchema.validate || {};
327
+ if (field.minItems !== void 0) formanSchema.validate.minItems = field.minItems;
328
+ if (field.maxItems !== void 0) formanSchema.validate.maxItems = field.maxItems;
329
+ }
330
+ return formanSchema;
90
331
  case "string":
91
332
  if (field.enum) {
92
333
  return {
93
334
  type: "select",
94
- help: field.description,
335
+ label: noEmpty(field.title),
336
+ help: noEmpty(field.description),
95
337
  options: field.enum.map((value) => ({ value }))
96
338
  };
339
+ } else if (field.oneOf) {
340
+ return {
341
+ type: "select",
342
+ label: noEmpty(field.title),
343
+ help: noEmpty(field.description),
344
+ options: field.oneOf.filter((value) => value).map((value) => ({ value: value.const }))
345
+ };
97
346
  }
98
- return {
347
+ const textField = {
99
348
  type: "text",
100
- help: field.description,
349
+ label: noEmpty(field.title),
350
+ help: noEmpty(field.description),
101
351
  default: field.default
102
352
  };
353
+ if (field.pattern || field.enum) {
354
+ textField.validate = textField.validate || {};
355
+ if (field.pattern) textField.validate.pattern = field.pattern;
356
+ if (field.enum) textField.validate.enum = field.enum;
357
+ }
358
+ return textField;
103
359
  default:
104
- return {
105
- type: JSON_PRIMITIVE_TYPE_MAP[field.type],
360
+ const primitiveField = {
361
+ type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "text",
106
362
  help: field.description,
107
363
  default: field.default
108
364
  };
365
+ if (field.minimum !== void 0 || field.maximum !== void 0) {
366
+ primitiveField.validate = primitiveField.validate || {};
367
+ if (field.minimum !== void 0) primitiveField.validate.min = field.minimum;
368
+ if (field.maximum !== void 0) primitiveField.validate.max = field.maximum;
369
+ }
370
+ return primitiveField;
109
371
  }
110
372
  }
373
+
374
+ // src/index.ts
375
+ function toJSONSchema(field) {
376
+ return toJSONSchemaInternal(field);
377
+ }
111
378
  export {
112
379
  toFormanSchema,
113
380
  toJSONSchema
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",
@@ -32,11 +32,12 @@
32
32
  "build": "tsup",
33
33
  "build:version": "node scripts/build-version.mjs",
34
34
  "format": "npx prettier . --write",
35
- "publish:jsr": "npx jsr publish",
35
+ "publish:jsr": "npx jsr publish --allow-dirty",
36
36
  "lint": "tsc"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@jest/globals": "^29.7.0",
40
+ "@types/json-schema": "^7.0.15",
40
41
  "@types/node": "^22.13.10",
41
42
  "jest": "^29.7.0",
42
43
  "prettier": "^3.5.3",