@stackql/provider-utils 0.3.7 → 0.3.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackql/provider-utils",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -172,12 +172,18 @@ export async function analyze(options) {
172
172
  const filepath = path.join(inputDir, filename);
173
173
  const spec = loadSpec(filepath);
174
174
 
175
+ const relevantVerbs = ["get", "put", "post", "patch", "delete"];
176
+
175
177
  for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
176
178
  for (const [verb, operation] of Object.entries(pathItem)) {
177
179
  if (typeof operation !== 'object' || operation === null) {
178
180
  continue;
179
181
  }
180
-
182
+ if(!relevantVerbs.includes(verb)) {
183
+ logger.info(`Skipping irrelevant operation: ${verb}`);
184
+ continue;
185
+ }
186
+
181
187
  // Then in the operation processing loop:
182
188
  const operationId = operation.operationId || '';
183
189
  // Check if operation is already mapped in CSV
@@ -154,7 +154,7 @@ function getAllRefs(obj) {
154
154
  }
155
155
 
156
156
  /**
157
- * Extract all $ref values from path-level parameters
157
+ * Extract all $ref values from path-level parameters and non-operation elements
158
158
  * @param {Object} pathItem - Path item from OpenAPI doc
159
159
  * @returns {Set<string>} - Set of refs
160
160
  */
@@ -166,15 +166,13 @@ function getPathLevelRefs(pathItem) {
166
166
  for (const param of pathItem.parameters) {
167
167
  if (param.$ref) {
168
168
  refs.add(param.$ref);
169
- if (param.$ref.includes('/parameters/')) {
170
- // Make sure we catch all parameters, especially path parameters
171
- const paramName = param.$ref.split('/').pop();
172
- if (paramName) {
173
- refs.add(`#/components/parameters/${paramName}`);
174
- }
175
- }
176
169
  } else if (typeof param === 'object') {
177
170
  // Extract refs from schema if present
171
+ if (param.schema && param.schema.$ref) {
172
+ refs.add(param.schema.$ref);
173
+ }
174
+
175
+ // Also get all nested refs in the parameter object
178
176
  for (const ref of getAllRefs(param)) {
179
177
  refs.add(ref);
180
178
  }
@@ -182,6 +180,15 @@ function getPathLevelRefs(pathItem) {
182
180
  }
183
181
  }
184
182
 
183
+ // Also check other non-operation properties for refs
184
+ for (const key in pathItem) {
185
+ if (!OPERATIONS.includes(key)) {
186
+ for (const ref of getAllRefs(pathItem[key])) {
187
+ refs.add(ref);
188
+ }
189
+ }
190
+ }
191
+
185
192
  return refs;
186
193
  }
187
194
 
@@ -251,6 +258,73 @@ function addMissingObjectTypes(obj) {
251
258
  return obj;
252
259
  }
253
260
 
261
+ /**
262
+ * Recursively resolve and add all references to service components
263
+ * @param {Set<string>} refs - Set of references to resolve
264
+ * @param {Object} service - Service object to add components to
265
+ * @param {Object} components - Source components from API doc
266
+ * @param {boolean} debug - Debug flag
267
+ * @param {Set<string>} processed - Set of already processed refs (to prevent infinite recursion)
268
+ */
269
+ function resolveReferences(refs, service, components, debug, processed = new Set()) {
270
+ let newRefs = new Set();
271
+
272
+ for (const ref of refs) {
273
+ // Skip if already processed
274
+ if (processed.has(ref)) {
275
+ continue;
276
+ }
277
+
278
+ processed.add(ref);
279
+
280
+ const parts = ref.split('/');
281
+
282
+ // Only process refs that point to components
283
+ if (parts.length >= 4 && parts[1] === "components") {
284
+ const componentType = parts[2];
285
+ const componentName = parts[3];
286
+
287
+ // Check if component type exists in service
288
+ if (!service.components[componentType]) {
289
+ service.components[componentType] = {};
290
+ }
291
+
292
+ // Skip if component already added
293
+ if (service.components[componentType][componentName]) {
294
+ continue;
295
+ }
296
+
297
+ // Add component if it exists in source document
298
+ if (components[componentType] && components[componentType][componentName]) {
299
+ service.components[componentType][componentName] =
300
+ JSON.parse(JSON.stringify(components[componentType][componentName]));
301
+
302
+ if (debug) {
303
+ logger.debug(`Added component ${componentType}/${componentName}`);
304
+ }
305
+
306
+ // Find all refs in the newly added component
307
+ const componentRefs = getAllRefs(service.components[componentType][componentName]);
308
+ for (const cRef of componentRefs) {
309
+ if (!processed.has(cRef)) {
310
+ newRefs.add(cRef);
311
+ }
312
+ }
313
+ } else if (debug) {
314
+ logger.debug(`WARNING: Could not find component ${componentType}/${componentName}`);
315
+ }
316
+ }
317
+ }
318
+
319
+ // If we found new refs, resolve them too (recursively)
320
+ if (newRefs.size > 0) {
321
+ if (debug) {
322
+ logger.debug(`Found ${newRefs.size} additional refs to resolve`);
323
+ }
324
+ resolveReferences(newRefs, service, components, debug, processed);
325
+ }
326
+ }
327
+
254
328
  /**
255
329
  * Split OpenAPI document into service-specific files
256
330
  * @param {Object} options - Options for splitting
@@ -302,7 +376,7 @@ export async function split(options) {
302
376
  const services = {};
303
377
  let opCounter = 0;
304
378
 
305
- // Process each path
379
+ // First pass: identify all services and collect operations
306
380
  for (const [pathKey, pathItem] of Object.entries(apiPaths)) {
307
381
  if (verbose) {
308
382
  logger.debug(`Processing path ${pathKey}`);
@@ -312,6 +386,9 @@ export async function split(options) {
312
386
  continue;
313
387
  }
314
388
 
389
+ // Collect all services that use this path
390
+ const pathServices = new Set();
391
+
315
392
  // Process each operation (HTTP verb)
316
393
  for (const [verbKey, opItem] of Object.entries(pathItem)) {
317
394
  if (!OPERATIONS.includes(verbKey) || !opItem) {
@@ -328,7 +405,7 @@ export async function split(options) {
328
405
  }
329
406
 
330
407
  // Skip excluded operations
331
- if (isOperationExcluded(excludeList, opItem, svcDiscriminator)) {
408
+ if (isOperationExcluded(excludeList, opItem)) {
332
409
  continue;
333
410
  }
334
411
 
@@ -344,6 +421,8 @@ export async function split(options) {
344
421
  continue;
345
422
  }
346
423
 
424
+ pathServices.add(service);
425
+
347
426
  if (verbose) {
348
427
  logger.debug(`Service name: ${service}`);
349
428
  logger.debug(`Service desc: ${serviceDesc}`);
@@ -375,104 +454,58 @@ export async function split(options) {
375
454
  opItem['x-github'].subcategory
376
455
  );
377
456
  }
378
-
379
- // Get all refs for operation
380
- const opRefs = getAllRefs(opItem);
381
-
382
- if (verbose) {
383
- logger.debug(`Found ${opRefs.size} refs for ${service}`);
384
- }
385
-
386
- // Add refs to components
387
- addRefsToComponents(opRefs, services[service], apiDocObj.components || {}, verbose);
388
-
389
- // Get internal refs
390
- for (let i = 0; i < 3; i++) { // Internal ref depth
391
- const intRefs = getAllRefs(services[service].components);
392
- if (verbose) {
393
- logger.debug(`Found ${intRefs.size} INTERNAL refs for service ${service}`);
394
- }
395
- addRefsToComponents(intRefs, services[service], apiDocObj.components || {}, verbose);
396
- }
397
-
398
- // Get deeply nested schema refs
399
- for (let i = 0; i < 10; i++) { // Schema max ref depth
400
- const intRefs = getAllRefs(services[service].components);
401
- // Filter refs that are already in service components
402
- const filteredRefs = new Set();
403
- for (const ref of intRefs) {
404
- const parts = ref.split('/');
405
- if (parts.length >= 4 && parts[1] === "components" && parts[2] === "schemas" &&
406
- !services[service].components.schemas[parts[3]]) {
407
- filteredRefs.add(ref);
408
- }
409
- }
410
-
411
- if (verbose) {
412
- logger.debug(`Found ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
413
- }
414
-
415
- if (filteredRefs.size > 0) {
416
- if (verbose) {
417
- logger.debug(`Adding ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
418
- }
419
- addRefsToComponents(filteredRefs, services[service], apiDocObj.components || {}, verbose);
420
- } else {
421
- if (verbose) {
422
- logger.debug(`Exiting INTERNAL schema refs for ${service}`);
423
- }
424
- break;
425
- }
426
- }
427
457
  }
428
458
 
429
- // After processing all operations in the path, collect path-level refs
430
- const pathRefs = getPathLevelRefs(pathItem);
431
-
432
- // Add path-level refs to all services that use this path
433
- for (const svcName in services) {
434
- if (services[svcName].paths[pathKey]) {
435
- if (verbose) {
436
- logger.debug(`Adding path-level refs for ${pathKey} to service ${svcName}`);
437
- }
438
-
439
- // Copy path-level parameters if they exist
440
- if (pathItem.parameters) {
441
- services[svcName].paths[pathKey].parameters = pathItem.parameters;
459
+ // For each service that uses this path, add path-level parameters
460
+ for (const service of pathServices) {
461
+ // Copy non-operation elements (like parameters) to service paths
462
+ for (const key in pathItem) {
463
+ if (!OPERATIONS.includes(key)) {
464
+ if (!services[service].paths[pathKey]) {
465
+ services[service].paths[pathKey] = {};
466
+ }
467
+ services[service].paths[pathKey][key] = pathItem[key];
442
468
  }
443
-
444
- // Add references from path-level parameters
445
- addRefsToComponents(pathRefs, services[svcName], apiDocObj.components || {}, verbose);
446
469
  }
447
470
  }
448
471
  }
449
472
 
450
- // Add non-operations to each service
473
+ // Second pass: collect all references for each service
451
474
  for (const service in services) {
452
- for (const pathKey of Object.keys(services[service].paths)) {
453
- if (verbose) {
454
- logger.debug(`Adding non operations to ${service} for path ${pathKey}`);
475
+ if (verbose) {
476
+ logger.debug(`Collecting references for service ${service}`);
477
+ }
478
+
479
+ // Get all refs from all operations in this service
480
+ const allRefs = new Set();
481
+
482
+ // Collect refs from paths
483
+ for (const pathKey in services[service].paths) {
484
+ const pathItem = services[service].paths[pathKey];
485
+
486
+ // Get refs from path-level parameters
487
+ const pathRefs = getPathLevelRefs(pathItem);
488
+ for (const ref of pathRefs) {
489
+ allRefs.add(ref);
455
490
  }
456
491
 
457
- for (const nonOp of NON_OPERATIONS) {
458
- if (verbose) {
459
- logger.debug(`Looking for non operation ${nonOp} in ${service} under path ${pathKey}`);
460
- }
461
-
462
- if (apiPaths[pathKey] && apiPaths[pathKey][nonOp]) {
463
- if (verbose) {
464
- logger.debug(`Adding ${nonOp} to ${service} for path ${pathKey}`);
465
- }
466
-
467
- // Special case for parameters
468
- if (nonOp === 'parameters') {
469
- for (const verbKey in services[service].paths[pathKey]) {
470
- services[service].paths[pathKey][verbKey].parameters = apiPaths[pathKey].parameters;
471
- }
492
+ // Get refs from operations
493
+ for (const verbKey in pathItem) {
494
+ if (OPERATIONS.includes(verbKey)) {
495
+ const opRefs = getAllRefs(pathItem[verbKey]);
496
+ for (const ref of opRefs) {
497
+ allRefs.add(ref);
472
498
  }
473
499
  }
474
500
  }
475
501
  }
502
+
503
+ if (verbose) {
504
+ logger.debug(`Found ${allRefs.size} total refs for service ${service}`);
505
+ }
506
+
507
+ // Resolve all references recursively
508
+ resolveReferences(allRefs, services[service], apiDocObj.components || {}, verbose);
476
509
  }
477
510
 
478
511
  // Update path param names (replace hyphens with underscores)
@@ -524,6 +557,15 @@ export async function split(options) {
524
557
  services[service].components = addMissingObjectTypes(services[service].components);
525
558
  }
526
559
 
560
+ // Cleanup empty components
561
+ for (const service in services) {
562
+ for (const componentType in services[service].components) {
563
+ if (Object.keys(services[service].components[componentType]).length === 0) {
564
+ delete services[service].components[componentType];
565
+ }
566
+ }
567
+ }
568
+
527
569
  // Write out service docs
528
570
  for (const service in services) {
529
571
  logger.info(`✅ Writing out OpenAPI doc for [${service}]`);