@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 +92 -1
- package/dist/index.cjs +330 -60
- package/dist/index.d.cts +79 -16
- package/dist/index.d.ts +79 -16
- package/dist/index.js +329 -61
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -1 +1,92 @@
|
|
|
1
|
-
# Forman Schema
|
|
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
|
-
|
|
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
|
-
type: "collection",
|
|
71
|
-
spec: field.spec
|
|
72
|
-
} : field.spec
|
|
73
|
-
)
|
|
74
|
-
};
|
|
156
|
+
return handleArrayType(normalizedField, result, context);
|
|
75
157
|
case "select":
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
335
|
+
label: noEmpty(field.title),
|
|
336
|
+
help: noEmpty(field.description),
|
|
101
337
|
spec
|
|
102
338
|
};
|
|
103
339
|
case "array":
|
|
104
|
-
|
|
340
|
+
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
341
|
+
const formanSchema = {
|
|
105
342
|
type: "array",
|
|
106
|
-
|
|
107
|
-
|
|
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 =
|
|
348
|
+
subField.required = items.required?.includes(name) || false;
|
|
111
349
|
return subField;
|
|
112
|
-
}) : toFormanSchema(
|
|
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
|
-
|
|
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
|
-
|
|
374
|
+
const textField = {
|
|
123
375
|
type: "text",
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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/
|
|
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
|
-
type: "collection",
|
|
46
|
-
spec: field.spec
|
|
47
|
-
} : field.spec
|
|
48
|
-
)
|
|
49
|
-
};
|
|
129
|
+
return handleArrayType(normalizedField, result, context);
|
|
50
130
|
case "select":
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
308
|
+
label: noEmpty(field.title),
|
|
309
|
+
help: noEmpty(field.description),
|
|
76
310
|
spec
|
|
77
311
|
};
|
|
78
312
|
case "array":
|
|
79
|
-
|
|
313
|
+
const items = field.items && isObject(field.items) ? field.items : void 0;
|
|
314
|
+
const formanSchema = {
|
|
80
315
|
type: "array",
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
321
|
+
subField.required = items.required?.includes(name) || false;
|
|
86
322
|
return subField;
|
|
87
|
-
}) : toFormanSchema(
|
|
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
|
-
|
|
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
|
-
|
|
347
|
+
const textField = {
|
|
98
348
|
type: "text",
|
|
99
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
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",
|