@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 +330 -61
- package/dist/index.d.cts +64 -32
- package/dist/index.d.ts +64 -32
- package/dist/index.js +329 -62
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
335
|
+
label: noEmpty(field.title),
|
|
336
|
+
help: noEmpty(field.description),
|
|
102
337
|
spec
|
|
103
338
|
};
|
|
104
339
|
case "array":
|
|
105
|
-
|
|
340
|
+
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
341
|
+
const formanSchema = {
|
|
106
342
|
type: "array",
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
348
|
+
subField.required = items.required?.includes(name) || false;
|
|
112
349
|
return subField;
|
|
113
|
-
}) : toFormanSchema(
|
|
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
|
-
|
|
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
|
-
|
|
374
|
+
const textField = {
|
|
124
375
|
type: "text",
|
|
125
|
-
|
|
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
|
-
|
|
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:
|
|
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?:
|
|
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
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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):
|
|
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
|
|
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:
|
|
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?:
|
|
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
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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):
|
|
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
|
|
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/
|
|
2
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
15
|
-
if (!
|
|
16
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
308
|
+
label: noEmpty(field.title),
|
|
309
|
+
help: noEmpty(field.description),
|
|
77
310
|
spec
|
|
78
311
|
};
|
|
79
312
|
case "array":
|
|
80
|
-
|
|
313
|
+
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
314
|
+
const formanSchema = {
|
|
81
315
|
type: "array",
|
|
82
|
-
|
|
83
|
-
|
|
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 =
|
|
321
|
+
subField.required = items.required?.includes(name) || false;
|
|
87
322
|
return subField;
|
|
88
|
-
}) : toFormanSchema(
|
|
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
|
-
|
|
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
|
-
|
|
347
|
+
const textField = {
|
|
99
348
|
type: "text",
|
|
100
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|