@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.
@@ -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
- const findOperationFieldsWithOptions = (schema) => {
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
- operationFields.push(
45
- ...param.meta.options.map((opt) => opt.value)
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
- return readAndParseJson(baseSchemaPath, "base.json", errors);
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
- const operationFields = findOperationFieldsWithOptions(schema);
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);