@makehq/forman-schema 0.1.4 → 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/README.md CHANGED
@@ -1 +1,92 @@
1
- # Forman Schema Tools
1
+ # Forman Schema
2
+
3
+ A utility for converting between Forman Schema and JSON Schema.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @makehq/forman-schema
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Converting from Forman Schema to JSON Schema
14
+
15
+ ```typescript
16
+ import { toJSONSchema } from '@makehq/forman-schema';
17
+
18
+ const formanField = {
19
+ type: 'collection',
20
+ spec: [
21
+ {
22
+ name: 'name',
23
+ type: 'text',
24
+ required: true,
25
+ },
26
+ {
27
+ name: 'age',
28
+ type: 'number',
29
+ },
30
+ ],
31
+ };
32
+
33
+ const jsonSchema = toJSONSchema(formanField);
34
+ ```
35
+
36
+ ### Converting from JSON Schema to Forman Schema
37
+
38
+ ```typescript
39
+ import { toFormanSchema } from '@makehq/forman-schema';
40
+
41
+ const jsonSchemaField = {
42
+ type: 'object',
43
+ properties: {
44
+ name: {
45
+ type: 'string',
46
+ },
47
+ age: {
48
+ type: 'number',
49
+ },
50
+ },
51
+ required: ['name'],
52
+ };
53
+
54
+ const formanSchema = toFormanSchema(jsonSchemaField);
55
+ ```
56
+
57
+ ## Supported Types
58
+
59
+ ### Forman Schema Types
60
+
61
+ - text → string
62
+ - number → number
63
+ - boolean → boolean
64
+ - date → string
65
+ - json → string
66
+ - select → string with enum
67
+ - collection → object
68
+ - array → array
69
+
70
+ ### JSON Schema Types
71
+
72
+ - string → text
73
+ - number → number
74
+ - boolean → boolean
75
+ - object → collection
76
+ - array → array
77
+
78
+ ## Testing
79
+
80
+ To test the project:
81
+
82
+ ```bash
83
+ npm test
84
+ ```
85
+
86
+ ## Building
87
+
88
+ To build the project:
89
+
90
+ ```bash
91
+ npm run build # Builds both ESM and CJS versions
92
+ ```
package/dist/index.cjs CHANGED
@@ -24,72 +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
- type: "collection",
71
- spec: field.spec
72
- } : field.spec
73
- )
74
- };
156
+ return handleArrayType(normalizedField, result, context);
75
157
  case "select":
76
- return {
77
- type: "string",
78
- description: noEmpty(field.help),
79
- enum: (field.options || []).map((option) => option.value)
80
- };
158
+ case "account":
159
+ case "hook":
160
+ case "keychain":
161
+ case "datastore":
162
+ case "aiagent":
163
+ case "file":
164
+ return handleSelectType(normalizedField, result, context);
81
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
+ }
82
262
  return {
83
- type: FORMAN_PRIMITIVE_TYPE_MAP[field.type],
84
- default: field.default != "" && field.default != null ? field.default : void 0,
85
- description: noEmpty(field.help)
263
+ title: noEmpty(option.label),
264
+ const: option.value
86
265
  };
266
+ });
267
+ } else {
268
+ result.enum = (options || []).map((option) => option.value);
87
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;
88
316
  }
317
+
318
+ // src/json.ts
319
+ var JSON_PRIMITIVE_TYPE_MAP = {
320
+ string: "text",
321
+ number: "number",
322
+ boolean: "boolean"
323
+ };
89
324
  function toFormanSchema(field) {
90
325
  switch (field.type) {
91
326
  case "object":
92
- 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]) => {
93
328
  const subField = toFormanSchema(property);
94
329
  subField.name = name;
95
330
  subField.required = field.required?.includes(name) || false;
@@ -97,41 +332,76 @@ function toFormanSchema(field) {
97
332
  }) : [];
98
333
  return {
99
334
  type: "collection",
100
- help: field.description,
335
+ label: noEmpty(field.title),
336
+ help: noEmpty(field.description),
101
337
  spec
102
338
  };
103
339
  case "array":
104
- return {
340
+ const items = field.items && isObject(field.items) ? field.items : void 0;
341
+ const formanSchema = {
105
342
  type: "array",
106
- help: field.description,
107
- 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]) => {
108
346
  const subField = toFormanSchema(property);
109
347
  subField.name = name;
110
- subField.required = field.items?.required?.includes(name) || false;
348
+ subField.required = items.required?.includes(name) || false;
111
349
  return subField;
112
- }) : toFormanSchema(field.items))
350
+ }) : toFormanSchema(items) : void 0
113
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;
114
358
  case "string":
115
359
  if (field.enum) {
116
360
  return {
117
361
  type: "select",
118
- help: field.description,
362
+ label: noEmpty(field.title),
363
+ help: noEmpty(field.description),
119
364
  options: field.enum.map((value) => ({ value }))
120
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
+ };
121
373
  }
122
- return {
374
+ const textField = {
123
375
  type: "text",
124
- help: field.description,
376
+ label: noEmpty(field.title),
377
+ help: noEmpty(field.description),
125
378
  default: field.default
126
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;
127
386
  default:
128
- return {
129
- type: JSON_PRIMITIVE_TYPE_MAP[field.type],
387
+ const primitiveField = {
388
+ type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "text",
130
389
  help: field.description,
131
390
  default: field.default
132
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;
133
398
  }
134
399
  }
400
+
401
+ // src/index.ts
402
+ function toJSONSchema(field) {
403
+ return toJSONSchemaInternal(field);
404
+ }
135
405
  // Annotate the CommonJS export names for ESM import in node:
136
406
  0 && (module.exports = {
137
407
  toFormanSchema,
package/dist/index.d.cts CHANGED
@@ -1,24 +1,87 @@
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
+ }
25
+ /**
26
+ * Represents a field in Forman Schema format.
27
+ */
1
28
  type FormanSchemaField = {
29
+ /** Field name identifier */
2
30
  name?: string;
3
- type: string;
31
+ /** The field type (e.g., 'text', 'number', 'boolean', 'collection', 'array', etc.) */
32
+ type: FormanSchemaFieldType;
33
+ /** Whether the field is required or not */
4
34
  required?: boolean;
5
- default?: string | number | boolean | null;
6
- options?: {
7
- value: string;
8
- }[];
35
+ /** Default value for the field */
36
+ default?: FormanSchemaValue;
37
+ /** Available options for select type fields */
38
+ options?: FormanSchemaOption[] | FormanSchemaExtendedOptions | string;
39
+ /** Help text or description for the field */
9
40
  help?: string;
41
+ /** Sub-fields specification for collection or array types */
10
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;
11
60
  };
12
- type JSONSchemaField = {
13
- type: string;
14
- description?: string;
15
- default?: string | number | boolean | null;
16
- enum?: string[];
17
- properties?: Record<string, JSONSchemaField>;
18
- items?: JSONSchemaField;
19
- required?: string[];
61
+ type FormanSchemaExtendedOptions = {
62
+ /** Store for the options */
63
+ store: FormanSchemaOption[] | string;
64
+ /** Nested fields for every option */
65
+ nested?: FormanSchemaNested;
20
66
  };
21
- declare function toJSONSchema(field: FormanSchemaField): JSONSchemaField;
22
- declare function toFormanSchema(field: JSONSchemaField): FormanSchemaField;
67
+ type FormanSchemaNested = FormanSchemaField[] | string | FormanSchemaExtendedNested;
68
+ type FormanSchemaExtendedNested = {
69
+ store: FormanSchemaField[] | string;
70
+ domain?: string;
71
+ };
72
+
73
+ /**
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
77
+ */
78
+ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
79
+
80
+ /**
81
+ * Converts a Forman Schema field to its JSON Schema equivalent.
82
+ * @param field The Forman Schema field to convert
83
+ * @returns The equivalent JSON Schema field
84
+ */
85
+ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
23
86
 
24
- 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,24 +1,87 @@
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
+ }
25
+ /**
26
+ * Represents a field in Forman Schema format.
27
+ */
1
28
  type FormanSchemaField = {
29
+ /** Field name identifier */
2
30
  name?: string;
3
- type: string;
31
+ /** The field type (e.g., 'text', 'number', 'boolean', 'collection', 'array', etc.) */
32
+ type: FormanSchemaFieldType;
33
+ /** Whether the field is required or not */
4
34
  required?: boolean;
5
- default?: string | number | boolean | null;
6
- options?: {
7
- value: string;
8
- }[];
35
+ /** Default value for the field */
36
+ default?: FormanSchemaValue;
37
+ /** Available options for select type fields */
38
+ options?: FormanSchemaOption[] | FormanSchemaExtendedOptions | string;
39
+ /** Help text or description for the field */
9
40
  help?: string;
41
+ /** Sub-fields specification for collection or array types */
10
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;
11
60
  };
12
- type JSONSchemaField = {
13
- type: string;
14
- description?: string;
15
- default?: string | number | boolean | null;
16
- enum?: string[];
17
- properties?: Record<string, JSONSchemaField>;
18
- items?: JSONSchemaField;
19
- required?: string[];
61
+ type FormanSchemaExtendedOptions = {
62
+ /** Store for the options */
63
+ store: FormanSchemaOption[] | string;
64
+ /** Nested fields for every option */
65
+ nested?: FormanSchemaNested;
20
66
  };
21
- declare function toJSONSchema(field: FormanSchemaField): JSONSchemaField;
22
- declare function toFormanSchema(field: JSONSchemaField): FormanSchemaField;
67
+ type FormanSchemaNested = FormanSchemaField[] | string | FormanSchemaExtendedNested;
68
+ type FormanSchemaExtendedNested = {
69
+ store: FormanSchemaField[] | string;
70
+ domain?: string;
71
+ };
72
+
73
+ /**
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
77
+ */
78
+ declare function toFormanSchema(field: JSONSchema7): FormanSchemaField;
79
+
80
+ /**
81
+ * Converts a Forman Schema field to its JSON Schema equivalent.
82
+ * @param field The Forman Schema field to convert
83
+ * @returns The equivalent JSON Schema field
84
+ */
85
+ declare function toJSONSchema(field: FormanSchemaField): JSONSchema7;
23
86
 
24
- 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,70 +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
- type: "collection",
46
- spec: field.spec
47
- } : field.spec
48
- )
49
- };
129
+ return handleArrayType(normalizedField, result, context);
50
130
  case "select":
51
- return {
52
- type: "string",
53
- description: noEmpty(field.help),
54
- enum: (field.options || []).map((option) => option.value)
55
- };
131
+ case "account":
132
+ case "hook":
133
+ case "keychain":
134
+ case "datastore":
135
+ case "aiagent":
136
+ case "file":
137
+ return handleSelectType(normalizedField, result, context);
56
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
+ }
57
235
  return {
58
- type: FORMAN_PRIMITIVE_TYPE_MAP[field.type],
59
- default: field.default != "" && field.default != null ? field.default : void 0,
60
- description: noEmpty(field.help)
236
+ title: noEmpty(option.label),
237
+ const: option.value
61
238
  };
239
+ });
240
+ } else {
241
+ result.enum = (options || []).map((option) => option.value);
62
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;
63
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
+ };
64
297
  function toFormanSchema(field) {
65
298
  switch (field.type) {
66
299
  case "object":
67
- 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]) => {
68
301
  const subField = toFormanSchema(property);
69
302
  subField.name = name;
70
303
  subField.required = field.required?.includes(name) || false;
@@ -72,41 +305,76 @@ function toFormanSchema(field) {
72
305
  }) : [];
73
306
  return {
74
307
  type: "collection",
75
- help: field.description,
308
+ label: noEmpty(field.title),
309
+ help: noEmpty(field.description),
76
310
  spec
77
311
  };
78
312
  case "array":
79
- return {
313
+ const items = field.items && isObject(field.items) ? field.items : void 0;
314
+ const formanSchema = {
80
315
  type: "array",
81
- help: field.description,
82
- 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]) => {
83
319
  const subField = toFormanSchema(property);
84
320
  subField.name = name;
85
- subField.required = field.items?.required?.includes(name) || false;
321
+ subField.required = items.required?.includes(name) || false;
86
322
  return subField;
87
- }) : toFormanSchema(field.items))
323
+ }) : toFormanSchema(items) : void 0
88
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;
89
331
  case "string":
90
332
  if (field.enum) {
91
333
  return {
92
334
  type: "select",
93
- help: field.description,
335
+ label: noEmpty(field.title),
336
+ help: noEmpty(field.description),
94
337
  options: field.enum.map((value) => ({ value }))
95
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
+ };
96
346
  }
97
- return {
347
+ const textField = {
98
348
  type: "text",
99
- help: field.description,
349
+ label: noEmpty(field.title),
350
+ help: noEmpty(field.description),
100
351
  default: field.default
101
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;
102
359
  default:
103
- return {
104
- type: JSON_PRIMITIVE_TYPE_MAP[field.type],
360
+ const primitiveField = {
361
+ type: JSON_PRIMITIVE_TYPE_MAP[field.type] || "text",
105
362
  help: field.description,
106
363
  default: field.default
107
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;
108
371
  }
109
372
  }
373
+
374
+ // src/index.ts
375
+ function toJSONSchema(field) {
376
+ return toJSONSchemaInternal(field);
377
+ }
110
378
  export {
111
379
  toFormanSchema,
112
380
  toJSONSchema
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@makehq/forman-schema",
3
- "version": "0.1.4",
3
+ "version": "1.1.0",
4
4
  "description": "Forman Schema Tools",
5
5
  "license": "MIT",
6
6
  "author": "Make",
7
7
  "repository": "github:integromat/forman-schema",
8
8
  "homepage": "https://www.make.com",
9
9
  "type": "module",
10
- "main": "dist/index.js",
11
- "types": "dist/index.d.cts",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.cts",
12
12
  "exports": {
13
13
  ".": {
14
14
  "import": {
@@ -30,13 +30,14 @@
30
30
  "scripts": {
31
31
  "test": "jest --runInBand --forceExit --verbose false",
32
32
  "build": "tsup",
33
- "lint": "tsc",
34
- "check-exports": "attw --pack .",
35
- "format": "npx prettier . --write"
33
+ "build:version": "node scripts/build-version.mjs",
34
+ "format": "npx prettier . --write",
35
+ "publish:jsr": "npx jsr publish --allow-dirty",
36
+ "lint": "tsc"
36
37
  },
37
38
  "devDependencies": {
38
- "@arethetypeswrong/cli": "^0.17.4",
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",