@boltic/cli 1.0.6-beta.8 → 1.0.7
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 +505 -60
- package/commands/integration.js +229 -33
- package/helper/validation.js +350 -26
- package/llm.txt +295 -0
- package/package.json +3 -3
- package/templates/component-schemas.js +945 -0
- package/templates/schemas.js +27 -15
- package/.claude/settings.local.json +0 -17
package/helper/validation.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import isEmpty from "lodash.isempty";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import * as componentSchemas from "../templates/component-schemas.js";
|
|
5
|
+
|
|
6
|
+
const validateOptionObject = (options, fieldName, fileLabel, errors) => {
|
|
7
|
+
options.forEach((opt, index) => {
|
|
8
|
+
const missingKeys = ["label", "value", "description"].filter(
|
|
9
|
+
(key) => !(key in opt)
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
if (missingKeys.length > 0) {
|
|
13
|
+
errors.add(
|
|
14
|
+
`"${fieldName}" field in "${fileLabel}" has an option at index ${index} missing keys: ${missingKeys.join(", ")}.`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
};
|
|
4
19
|
|
|
5
20
|
const readAndParseJson = (filePath, fileLabel, errors) => {
|
|
6
21
|
try {
|
|
@@ -15,8 +30,7 @@ const readAndParseJson = (filePath, fileLabel, errors) => {
|
|
|
15
30
|
return null;
|
|
16
31
|
}
|
|
17
32
|
};
|
|
18
|
-
|
|
19
|
-
const findResourceFieldsWithOptions = (schema) => {
|
|
33
|
+
const findResourceFieldsWithOptions = (schema, fileLabel, errors) => {
|
|
20
34
|
const resourceFields = [];
|
|
21
35
|
if (Array.isArray(schema?.parameters)) {
|
|
22
36
|
schema.parameters.forEach((param) => {
|
|
@@ -24,6 +38,12 @@ const findResourceFieldsWithOptions = (schema) => {
|
|
|
24
38
|
param.name === "resource" &&
|
|
25
39
|
Array.isArray(param.meta?.options)
|
|
26
40
|
) {
|
|
41
|
+
validateOptionObject(
|
|
42
|
+
param.meta.options,
|
|
43
|
+
"resource",
|
|
44
|
+
fileLabel,
|
|
45
|
+
errors
|
|
46
|
+
);
|
|
27
47
|
resourceFields.push(
|
|
28
48
|
...param.meta.options.map((opt) => opt.value)
|
|
29
49
|
);
|
|
@@ -32,24 +52,257 @@ const findResourceFieldsWithOptions = (schema) => {
|
|
|
32
52
|
}
|
|
33
53
|
return resourceFields;
|
|
34
54
|
};
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
const findOperationFieldsWithOptions = (
|
|
56
|
+
schema,
|
|
57
|
+
fileLabel,
|
|
58
|
+
errors,
|
|
59
|
+
expectedResourceName
|
|
60
|
+
) => {
|
|
37
61
|
const operationFields = [];
|
|
62
|
+
|
|
38
63
|
if (Array.isArray(schema?.parameters)) {
|
|
39
64
|
schema.parameters.forEach((param) => {
|
|
40
65
|
if (
|
|
41
66
|
param.name === "operation" &&
|
|
42
67
|
Array.isArray(param.meta?.options)
|
|
43
68
|
) {
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
validateOptionObject(
|
|
70
|
+
param.meta.options,
|
|
71
|
+
"operation",
|
|
72
|
+
fileLabel,
|
|
73
|
+
errors
|
|
46
74
|
);
|
|
75
|
+
param.meta.options.forEach((opt, index) => {
|
|
76
|
+
if (typeof opt.value === "string") {
|
|
77
|
+
const parts = opt.value.split(".");
|
|
78
|
+
if (parts.length < 2) {
|
|
79
|
+
errors.add(
|
|
80
|
+
`"operation" field in "${fileLabel}" has an invalid option at index ${index} with value "${opt.value}". Expected format "resource.operation".`
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
const resource = parts[0];
|
|
84
|
+
if (resource !== expectedResourceName) {
|
|
85
|
+
errors.add(
|
|
86
|
+
`"operation" field in "${fileLabel}" has an inconsistent resource prefix at index ${index}. Found "${resource}" but expected "${expectedResourceName}".`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
operationFields.push(opt.value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
47
93
|
}
|
|
48
94
|
});
|
|
49
95
|
}
|
|
96
|
+
|
|
50
97
|
return operationFields;
|
|
51
98
|
};
|
|
52
99
|
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// VALIDATE COMPONENT SCHEMAS
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract all possible keys from a component schema structure recursively
|
|
106
|
+
* @param {Object} obj - The object to extract keys from
|
|
107
|
+
* @param {string} prefix - Current key prefix for nested objects
|
|
108
|
+
* @returns {Set<string>} Set of all allowed keys with dot notation for nested paths
|
|
109
|
+
*/
|
|
110
|
+
const extractAllowedKeys = (obj, prefix = "") => {
|
|
111
|
+
const allowedKeys = new Set();
|
|
112
|
+
|
|
113
|
+
Object.keys(obj).forEach((key) => {
|
|
114
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
115
|
+
allowedKeys.add(fullKey);
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
typeof obj[key] === "object" &&
|
|
119
|
+
obj[key] !== null &&
|
|
120
|
+
!Array.isArray(obj[key])
|
|
121
|
+
) {
|
|
122
|
+
const nestedKeys = extractAllowedKeys(obj[key], fullKey);
|
|
123
|
+
nestedKeys.forEach((nestedKey) => allowedKeys.add(nestedKey));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return allowedKeys;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate that a schema object doesn't contain any extra keys
|
|
132
|
+
* @param {Object} schemaObj - The schema object to validate
|
|
133
|
+
* @param {Set<string>} allowedKeys - Set of allowed keys
|
|
134
|
+
* @param {string} schemaName - Name of the schema for error messages
|
|
135
|
+
* @param {string} displayType - The display type for error messages
|
|
136
|
+
* @param {Set} errors - Error collection
|
|
137
|
+
* @param {string} prefix - Current key prefix for nested objects
|
|
138
|
+
*/
|
|
139
|
+
const validateSchemaKeys = (
|
|
140
|
+
schemaObj,
|
|
141
|
+
allowedKeys,
|
|
142
|
+
schemaName,
|
|
143
|
+
displayType,
|
|
144
|
+
errors,
|
|
145
|
+
prefix = "",
|
|
146
|
+
filename = ""
|
|
147
|
+
) => {
|
|
148
|
+
const fileLabel = filename ? ` in "${filename}"` : "";
|
|
149
|
+
|
|
150
|
+
Object.keys(schemaObj).forEach((key) => {
|
|
151
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
152
|
+
|
|
153
|
+
if (!allowedKeys.has(fullKey)) {
|
|
154
|
+
errors.add(
|
|
155
|
+
`"${schemaName}" has an invalid key "${fullKey}" for displayType "${displayType}"${fileLabel}.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
typeof schemaObj[key] === "object" &&
|
|
161
|
+
schemaObj[key] !== null &&
|
|
162
|
+
!Array.isArray(schemaObj[key])
|
|
163
|
+
) {
|
|
164
|
+
validateSchemaKeys(
|
|
165
|
+
schemaObj[key],
|
|
166
|
+
allowedKeys,
|
|
167
|
+
schemaName,
|
|
168
|
+
displayType,
|
|
169
|
+
errors,
|
|
170
|
+
fullKey,
|
|
171
|
+
filename
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate a single component schema against its component type definition
|
|
179
|
+
* @param {Object} schema - The schema to validate
|
|
180
|
+
* @param {string} displayType - The display type to validate against
|
|
181
|
+
* @param {Set} errors - Error collection
|
|
182
|
+
* @param {string} filename - The filename for error messages
|
|
183
|
+
*/
|
|
184
|
+
const validateComponentByType = (
|
|
185
|
+
schema,
|
|
186
|
+
displayType,
|
|
187
|
+
errors,
|
|
188
|
+
filename = ""
|
|
189
|
+
) => {
|
|
190
|
+
const fileLabel = filename ? ` in "${filename}"` : "";
|
|
191
|
+
|
|
192
|
+
// Get the component schema definition for this display type
|
|
193
|
+
const componentSchema = componentSchemas[displayType];
|
|
194
|
+
|
|
195
|
+
if (!componentSchema) {
|
|
196
|
+
errors.add(
|
|
197
|
+
`"${schema.name}" has an unsupported displayType "${displayType}"${fileLabel}.`
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!componentSchema.meta) {
|
|
203
|
+
errors.add(
|
|
204
|
+
`Component schema for "${displayType}" is missing meta definition${fileLabel}.`
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Extract allowed keys from the component schema
|
|
210
|
+
const allowedKeys = extractAllowedKeys(componentSchema.meta);
|
|
211
|
+
|
|
212
|
+
// Validate the schema meta object (excluding displayType which we already handled)
|
|
213
|
+
const { displayType: currentDisplayType, ...restMeta } = schema.meta;
|
|
214
|
+
validateSchemaKeys(
|
|
215
|
+
restMeta,
|
|
216
|
+
allowedKeys,
|
|
217
|
+
schema.name,
|
|
218
|
+
currentDisplayType,
|
|
219
|
+
errors,
|
|
220
|
+
"",
|
|
221
|
+
filename
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const validateComponentSchemas = (schemas, errors, filename = "") => {
|
|
226
|
+
const fileLabel = filename ? ` in "${filename}"` : "";
|
|
227
|
+
|
|
228
|
+
schemas.forEach((schema) => {
|
|
229
|
+
// Basic required field validation
|
|
230
|
+
if (!schema.name) {
|
|
231
|
+
errors.add(`Schema is missing a name${fileLabel}.`);
|
|
232
|
+
return; // Can't continue without a name
|
|
233
|
+
}
|
|
234
|
+
if (!schema.meta) {
|
|
235
|
+
errors.add(
|
|
236
|
+
`"${schema.name}" is missing a meta object${fileLabel}.`
|
|
237
|
+
);
|
|
238
|
+
return; // Can't continue without meta
|
|
239
|
+
}
|
|
240
|
+
if (!schema.meta.displayType) {
|
|
241
|
+
errors.add(
|
|
242
|
+
`"${schema.name}" is missing a displayType${fileLabel}.`
|
|
243
|
+
);
|
|
244
|
+
return; // Can't continue without displayType
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Optional field validation (these are warnings, not blocking)
|
|
248
|
+
if (!schema.meta.displayName) {
|
|
249
|
+
errors.add(
|
|
250
|
+
`"${schema.name}" is missing a displayName${fileLabel}.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Only require placeholder if the component schema defines it
|
|
255
|
+
const componentSchema = componentSchemas[schema.meta.displayType];
|
|
256
|
+
if (
|
|
257
|
+
componentSchema &&
|
|
258
|
+
componentSchema.meta &&
|
|
259
|
+
"placeholder" in componentSchema.meta &&
|
|
260
|
+
!schema.meta.placeholder
|
|
261
|
+
) {
|
|
262
|
+
errors.add(
|
|
263
|
+
`"${schema.name}" is missing a placeholder${fileLabel}.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Only require description if the component schema defines it
|
|
268
|
+
if (
|
|
269
|
+
componentSchema &&
|
|
270
|
+
componentSchema.meta &&
|
|
271
|
+
"description" in componentSchema.meta &&
|
|
272
|
+
!schema.meta.description
|
|
273
|
+
) {
|
|
274
|
+
errors.add(
|
|
275
|
+
`"${schema.name}" is missing a description${fileLabel}.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 🚨 Validate for duplicate options with same label and value
|
|
280
|
+
if (Array.isArray(schema.meta.options)) {
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
schema.meta.options.forEach((option, index) => {
|
|
283
|
+
if (option && typeof option === "object") {
|
|
284
|
+
const key = `${option.label}::${option.value}`;
|
|
285
|
+
if (seen.has(key)) {
|
|
286
|
+
errors.add(
|
|
287
|
+
`"${schema.name}" contains duplicate option at index ${index} with label "${option.label}" and value "${option.value}"${fileLabel}.`
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
seen.add(key);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Validate against the specific component type schema
|
|
297
|
+
validateComponentByType(
|
|
298
|
+
schema,
|
|
299
|
+
schema.meta.displayType,
|
|
300
|
+
errors,
|
|
301
|
+
filename
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
53
306
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
307
|
// INDIVIDUAL VALIDATORS
|
|
55
308
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -83,6 +336,22 @@ const validateWebhook = (webhookPath, spec, errors) => {
|
|
|
83
336
|
`"webhook.json" exists but trigger_type is not defined in spec.json.`
|
|
84
337
|
);
|
|
85
338
|
}
|
|
339
|
+
|
|
340
|
+
// Validate webhook schema parameters if webhook exists
|
|
341
|
+
if (hasWebhook) {
|
|
342
|
+
const webhookSchema = readAndParseJson(
|
|
343
|
+
webhookPath,
|
|
344
|
+
"webhook.json",
|
|
345
|
+
errors
|
|
346
|
+
);
|
|
347
|
+
if (webhookSchema && Array.isArray(webhookSchema.parameters)) {
|
|
348
|
+
validateComponentSchemas(
|
|
349
|
+
webhookSchema.parameters,
|
|
350
|
+
errors,
|
|
351
|
+
"webhook.json"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
86
355
|
};
|
|
87
356
|
|
|
88
357
|
const validateBaseSchema = (baseSchemaPath, errors) => {
|
|
@@ -90,7 +359,54 @@ const validateBaseSchema = (baseSchemaPath, errors) => {
|
|
|
90
359
|
errors.add(`"base.json" not found in the "schemas" directory.`);
|
|
91
360
|
return null;
|
|
92
361
|
}
|
|
93
|
-
|
|
362
|
+
|
|
363
|
+
const baseSchema = readAndParseJson(baseSchemaPath, "base.json", errors);
|
|
364
|
+
|
|
365
|
+
// Validate base schema parameters
|
|
366
|
+
if (baseSchema && Array.isArray(baseSchema.parameters)) {
|
|
367
|
+
validateComponentSchemas(baseSchema.parameters, errors, "base.json");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return baseSchema;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const validateAuthentication = (authPath, errors) => {
|
|
374
|
+
// Authentication is optional, so only validate if it exists
|
|
375
|
+
if (fs.existsSync(authPath)) {
|
|
376
|
+
const authSchema = readAndParseJson(
|
|
377
|
+
authPath,
|
|
378
|
+
"authentication.json",
|
|
379
|
+
errors
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Validate authentication schema parameters
|
|
383
|
+
if (authSchema && Array.isArray(authSchema.parameters)) {
|
|
384
|
+
validateComponentSchemas(
|
|
385
|
+
authSchema.parameters,
|
|
386
|
+
errors,
|
|
387
|
+
"authentication.json"
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Validate authentication type-specific parameters (like api_key, oauth, etc.)
|
|
392
|
+
if (authSchema) {
|
|
393
|
+
Object.keys(authSchema).forEach((key) => {
|
|
394
|
+
if (
|
|
395
|
+
key !== "parameters" &&
|
|
396
|
+
typeof authSchema[key] === "object" &&
|
|
397
|
+
authSchema[key] !== null
|
|
398
|
+
) {
|
|
399
|
+
if (Array.isArray(authSchema[key].parameters)) {
|
|
400
|
+
validateComponentSchemas(
|
|
401
|
+
authSchema[key].parameters,
|
|
402
|
+
errors,
|
|
403
|
+
"authentication.json"
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
94
410
|
};
|
|
95
411
|
|
|
96
412
|
const validateResources = (resourcesDir, resourceFields, errors) => {
|
|
@@ -121,7 +437,21 @@ const validateResources = (resourcesDir, resourceFields, errors) => {
|
|
|
121
437
|
);
|
|
122
438
|
if (!schema) return;
|
|
123
439
|
|
|
124
|
-
|
|
440
|
+
// Validate resource file parameters
|
|
441
|
+
if (Array.isArray(schema.parameters)) {
|
|
442
|
+
validateComponentSchemas(
|
|
443
|
+
schema.parameters,
|
|
444
|
+
errors,
|
|
445
|
+
`${resourceFile}.json`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const operationFields = findOperationFieldsWithOptions(
|
|
450
|
+
schema,
|
|
451
|
+
`${resourceFile}.json`,
|
|
452
|
+
errors,
|
|
453
|
+
resourceFile
|
|
454
|
+
);
|
|
125
455
|
|
|
126
456
|
operationFields.forEach((operation) => {
|
|
127
457
|
const operationMethod = operation.split(".")[1];
|
|
@@ -144,6 +474,15 @@ const validateResources = (resourcesDir, resourceFields, errors) => {
|
|
|
144
474
|
errors.add(
|
|
145
475
|
`Operation "${operationMethod}" in "${resourceFile}.json" is missing parameters.`
|
|
146
476
|
);
|
|
477
|
+
} else {
|
|
478
|
+
// Validate operation parameters using component schemas
|
|
479
|
+
if (Array.isArray(methodDef.parameters)) {
|
|
480
|
+
validateComponentSchemas(
|
|
481
|
+
methodDef.parameters,
|
|
482
|
+
errors,
|
|
483
|
+
`${resourceFile}.json`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
147
486
|
}
|
|
148
487
|
if (!methodDef.definition) {
|
|
149
488
|
errors.add(
|
|
@@ -154,23 +493,6 @@ const validateResources = (resourcesDir, resourceFields, errors) => {
|
|
|
154
493
|
});
|
|
155
494
|
};
|
|
156
495
|
|
|
157
|
-
// const validateParametersScema = (schema, errors) => {
|
|
158
|
-
// if (!schema || !Array.isArray(schema.parameters)) {
|
|
159
|
-
// errors.add(
|
|
160
|
-
// `Schema is missing or parameters are not defined as an array.`
|
|
161
|
-
// );
|
|
162
|
-
// return;
|
|
163
|
-
// }
|
|
164
|
-
// schema.parameters.forEach((param) => {
|
|
165
|
-
// const { meta } = param;
|
|
166
|
-
// if (!param.name) {
|
|
167
|
-
// errors.add(
|
|
168
|
-
// `Parameter in schema is missing a name. Ensure all parameters have a "name" field.`
|
|
169
|
-
// );
|
|
170
|
-
// }
|
|
171
|
-
// });
|
|
172
|
-
// };
|
|
173
|
-
|
|
174
496
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
175
497
|
// MAIN FUNCTION
|
|
176
498
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -184,6 +506,7 @@ export const validateIntegrationSchemas = (currentDir) => {
|
|
|
184
506
|
resources: path.join(currentDir, "schemas", "resources"),
|
|
185
507
|
spec: path.join(currentDir, "spec.json"),
|
|
186
508
|
webhook: path.join(currentDir, "schemas", "webhook.json"),
|
|
509
|
+
authentication: path.join(currentDir, "schemas", "authentication.json"),
|
|
187
510
|
documentation: path.join(currentDir, "Documentation.mdx"),
|
|
188
511
|
};
|
|
189
512
|
|
|
@@ -192,10 +515,11 @@ export const validateIntegrationSchemas = (currentDir) => {
|
|
|
192
515
|
|
|
193
516
|
const spec = validateSpec(paths.spec, errors);
|
|
194
517
|
validateWebhook(paths.webhook, spec, errors);
|
|
518
|
+
validateAuthentication(paths.authentication, errors);
|
|
195
519
|
|
|
196
520
|
const baseSchema = validateBaseSchema(paths.base, errors);
|
|
197
521
|
const resourceFields = baseSchema
|
|
198
|
-
? findResourceFieldsWithOptions(baseSchema)
|
|
522
|
+
? findResourceFieldsWithOptions(baseSchema, "base.json", errors)
|
|
199
523
|
: [];
|
|
200
524
|
|
|
201
525
|
validateResources(paths.resources, resourceFields, errors);
|