@stackql/provider-utils 0.3.8 → 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.8",
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",
@@ -154,35 +154,37 @@ 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
  */
161
161
  function getPathLevelRefs(pathItem) {
162
162
  const refs = new Set();
163
- const relevantVerbs = ["get", "put", "post", "patch", "delete"];
164
163
 
165
164
  // Check for path-level parameters
166
165
  if (pathItem.parameters) {
167
- // Only collect path parameters if they're used by relevant operations
168
- let hasRelevantOperation = false;
169
- for (const verb of relevantVerbs) {
170
- if (pathItem[verb] && typeof pathItem[verb] === 'object') {
171
- hasRelevantOperation = true;
172
- break;
166
+ for (const param of pathItem.parameters) {
167
+ if (param.$ref) {
168
+ refs.add(param.$ref);
169
+ } else if (typeof param === 'object') {
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
176
+ for (const ref of getAllRefs(param)) {
177
+ refs.add(ref);
178
+ }
173
179
  }
174
180
  }
175
-
176
- if (hasRelevantOperation) {
177
- for (const param of pathItem.parameters) {
178
- if (param.$ref) {
179
- refs.add(param.$ref);
180
- } else if (typeof param === 'object') {
181
- // Extract refs from schema if present
182
- for (const ref of getAllRefs(param)) {
183
- refs.add(ref);
184
- }
185
- }
181
+ }
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);
186
188
  }
187
189
  }
188
190
  }
@@ -256,6 +258,73 @@ function addMissingObjectTypes(obj) {
256
258
  return obj;
257
259
  }
258
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
+
259
328
  /**
260
329
  * Split OpenAPI document into service-specific files
261
330
  * @param {Object} options - Options for splitting
@@ -307,7 +376,7 @@ export async function split(options) {
307
376
  const services = {};
308
377
  let opCounter = 0;
309
378
 
310
- // Process each path
379
+ // First pass: identify all services and collect operations
311
380
  for (const [pathKey, pathItem] of Object.entries(apiPaths)) {
312
381
  if (verbose) {
313
382
  logger.debug(`Processing path ${pathKey}`);
@@ -317,6 +386,9 @@ export async function split(options) {
317
386
  continue;
318
387
  }
319
388
 
389
+ // Collect all services that use this path
390
+ const pathServices = new Set();
391
+
320
392
  // Process each operation (HTTP verb)
321
393
  for (const [verbKey, opItem] of Object.entries(pathItem)) {
322
394
  if (!OPERATIONS.includes(verbKey) || !opItem) {
@@ -333,7 +405,7 @@ export async function split(options) {
333
405
  }
334
406
 
335
407
  // Skip excluded operations
336
- if (isOperationExcluded(excludeList, opItem, svcDiscriminator)) {
408
+ if (isOperationExcluded(excludeList, opItem)) {
337
409
  continue;
338
410
  }
339
411
 
@@ -349,6 +421,8 @@ export async function split(options) {
349
421
  continue;
350
422
  }
351
423
 
424
+ pathServices.add(service);
425
+
352
426
  if (verbose) {
353
427
  logger.debug(`Service name: ${service}`);
354
428
  logger.debug(`Service desc: ${serviceDesc}`);
@@ -380,104 +454,58 @@ export async function split(options) {
380
454
  opItem['x-github'].subcategory
381
455
  );
382
456
  }
383
-
384
- // Get all refs for operation
385
- const opRefs = getAllRefs(opItem);
386
-
387
- if (verbose) {
388
- logger.debug(`Found ${opRefs.size} refs for ${service}`);
389
- }
390
-
391
- // Add refs to components
392
- addRefsToComponents(opRefs, services[service], apiDocObj.components || {}, verbose);
393
-
394
- // Get internal refs
395
- for (let i = 0; i < 3; i++) { // Internal ref depth
396
- const intRefs = getAllRefs(services[service].components);
397
- if (verbose) {
398
- logger.debug(`Found ${intRefs.size} INTERNAL refs for service ${service}`);
399
- }
400
- addRefsToComponents(intRefs, services[service], apiDocObj.components || {}, verbose);
401
- }
402
-
403
- // Get deeply nested schema refs
404
- for (let i = 0; i < 10; i++) { // Schema max ref depth
405
- const intRefs = getAllRefs(services[service].components);
406
- // Filter refs that are already in service components
407
- const filteredRefs = new Set();
408
- for (const ref of intRefs) {
409
- const parts = ref.split('/');
410
- if (parts.length >= 4 && parts[1] === "components" && parts[2] === "schemas" &&
411
- !services[service].components.schemas[parts[3]]) {
412
- filteredRefs.add(ref);
413
- }
414
- }
415
-
416
- if (verbose) {
417
- logger.debug(`Found ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
418
- }
419
-
420
- if (filteredRefs.size > 0) {
421
- if (verbose) {
422
- logger.debug(`Adding ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
423
- }
424
- addRefsToComponents(filteredRefs, services[service], apiDocObj.components || {}, verbose);
425
- } else {
426
- if (verbose) {
427
- logger.debug(`Exiting INTERNAL schema refs for ${service}`);
428
- }
429
- break;
430
- }
431
- }
432
457
  }
433
458
 
434
- // After processing all operations in the path, collect path-level refs
435
- const pathRefs = getPathLevelRefs(pathItem);
436
-
437
- // Add path-level refs to all services that use this path
438
- for (const svcName in services) {
439
- if (services[svcName].paths[pathKey]) {
440
- if (verbose) {
441
- logger.debug(`Adding path-level refs for ${pathKey} to service ${svcName}`);
442
- }
443
-
444
- // Copy path-level parameters if they exist
445
- if (pathItem.parameters) {
446
- 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];
447
468
  }
448
-
449
- // Add references from path-level parameters
450
- addRefsToComponents(pathRefs, services[svcName], apiDocObj.components || {}, verbose);
451
469
  }
452
470
  }
453
471
  }
454
472
 
455
- // Add non-operations to each service
473
+ // Second pass: collect all references for each service
456
474
  for (const service in services) {
457
- for (const pathKey of Object.keys(services[service].paths)) {
458
- if (verbose) {
459
- 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);
460
490
  }
461
491
 
462
- for (const nonOp of NON_OPERATIONS) {
463
- if (verbose) {
464
- logger.debug(`Looking for non operation ${nonOp} in ${service} under path ${pathKey}`);
465
- }
466
-
467
- if (apiPaths[pathKey] && apiPaths[pathKey][nonOp]) {
468
- if (verbose) {
469
- logger.debug(`Adding ${nonOp} to ${service} for path ${pathKey}`);
470
- }
471
-
472
- // Special case for parameters
473
- if (nonOp === 'parameters') {
474
- for (const verbKey in services[service].paths[pathKey]) {
475
- services[service].paths[pathKey][verbKey].parameters = apiPaths[pathKey].parameters;
476
- }
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);
477
498
  }
478
499
  }
479
500
  }
480
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);
481
509
  }
482
510
 
483
511
  // Update path param names (replace hyphens with underscores)
@@ -529,6 +557,15 @@ export async function split(options) {
529
557
  services[service].components = addMissingObjectTypes(services[service].components);
530
558
  }
531
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
+
532
569
  // Write out service docs
533
570
  for (const service in services) {
534
571
  logger.info(`✅ Writing out OpenAPI doc for [${service}]`);