@stackql/provider-utils 0.4.1 → 0.4.2

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # StackQL Provider Utils
2
2
 
3
- ![NPM Version](https://img.shields.io/npm/v/%40stackql%2Fprovider-utils) | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/stackql/stackql/total?style=plastic&label=stackql%20downloads)
3
+ ![NPM Version](https://img.shields.io/npm/v/%40stackql%2Fprovider-utils)
4
+ ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/stackql/stackql/total?style=plastic&label=stackql%20downloads)
4
5
 
5
6
  A comprehensive toolkit for transforming OpenAPI specifications into StackQL providers. This library streamlines the process of parsing, mapping, validating, testing, and generating documentation for StackQL providers.
6
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackql/provider-utils",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -4,6 +4,24 @@ import {
4
4
  sanitizeHtml
5
5
  } from '../helpers.js';
6
6
 
7
+ const getRequiredBodyParams = (methodDetails, accessType) => {
8
+ // Only process request body for insert, update, replace, and exec
9
+ if (!['insert', 'update', 'replace', 'exec'].includes(accessType)) {
10
+ return [];
11
+ }
12
+
13
+ // Get required body params if they exist
14
+ const requiredBodyProps = methodDetails.requestBody?.required ? methodDetails.requestBody.required : [];
15
+
16
+ // For insert, update, and replace, prefix with data__
17
+ if (['insert', 'update', 'replace'].includes(accessType)) {
18
+ return requiredBodyProps.map(prop => `data__${prop}`);
19
+ } else {
20
+ // For exec, don't prefix
21
+ return requiredBodyProps;
22
+ }
23
+ };
24
+
7
25
  export function createMethodsSection(resourceData, dereferencedAPI) {
8
26
 
9
27
  let content = `\n## Methods\n\n`;
@@ -37,10 +55,16 @@ export function createMethodsSection(resourceData, dereferencedAPI) {
37
55
  for (const [methodName, methodDetails] of Object.entries(methods)) {
38
56
  console.info(`Adding ${accessType} method to table: ${methodName}`);
39
57
 
58
+ // Get required params from both the standard params and the request body
59
+ const reqParamsArr = Object.keys(methodDetails.requiredParams || {});
60
+ const reqBodyParamsArr = getRequiredBodyParams(methodDetails, accessType);
61
+
62
+ // Combine both types of required parameters
63
+ const allReqParamsArr = [...reqParamsArr, ...reqBodyParamsArr];
64
+
40
65
  // Format required params as comma-delimited list with hyperlinks
41
- const requiredParamsArr = Object.keys(methodDetails.requiredParams || {});
42
- const requiredParamsStr = requiredParamsArr.length > 0
43
- ? requiredParamsArr.map(param => `<a href="#parameter-${param}"><code>${param}</code></a>`).join(', ')
66
+ const requiredParamsStr = allReqParamsArr.length > 0
67
+ ? allReqParamsArr.map(param => `<a href="#parameter-${param}"><code>${param}</code></a>`).join(', ')
44
68
  : '';
45
69
 
46
70
  // Format optional params as comma-delimited list with hyperlinks
@@ -48,7 +72,7 @@ export function createMethodsSection(resourceData, dereferencedAPI) {
48
72
  const optionalParamsStr = optionalParamsArr.length > 0
49
73
  ? optionalParamsArr.map(param => `<a href="#parameter-${param}"><code>${param}</code></a>`).join(', ')
50
74
  : '';
51
-
75
+
52
76
  // Add the method row to the table
53
77
  content += `
54
78
  <tr>
@@ -95,6 +95,23 @@ function findExistingMapping(spec, pathRef) {
95
95
  };
96
96
  }
97
97
 
98
+ /**
99
+ * Escape and sanitize a CSV field value
100
+ * @param {string} value - Field value to escape
101
+ * @returns {string} - Escaped value
102
+ */
103
+ function escapeCsvField(value) {
104
+ if (!value) return '';
105
+
106
+ // If the value contains commas, double quotes, or newlines, wrap it in quotes
107
+ // and escape any existing double quotes by doubling them
108
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
109
+ return `"${value.replace(/"/g, '""')}"`;
110
+ }
111
+
112
+ return value;
113
+ }
114
+
98
115
  /**
99
116
  * Analyze OpenAPI specs and generate mapping CSV
100
117
  * @param {Object} options - Options for analysis
@@ -159,7 +176,7 @@ export async function analyze(options) {
159
176
 
160
177
  // Only write header if creating a new file
161
178
  if (!fileExists) {
162
- writer.write('filename,path,operationId,formatted_op_id,verb,response_object,tags,formatted_tags,stackql_resource_name,stackql_method_name,stackql_verb\n');
179
+ writer.write('filename,path,operationId,formatted_op_id,verb,response_object,tags,formatted_tags,stackql_resource_name,stackql_method_name,stackql_verb,op_description\n');
163
180
  }
164
181
 
165
182
  const files = fs.readdirSync(inputDir);
@@ -216,15 +233,27 @@ export async function analyze(options) {
216
233
  // Find existing mapping if available
217
234
  const { resourceName, methodName, sqlVerb } = findExistingMapping(spec, pathRef);
218
235
 
219
- // Escape commas in fields
220
- const escapedPath = pathKey.includes(',') ? `"${pathKey}"` : pathKey;
221
- const escapedOperationId = operationId.includes(',') ? `"${operationId}"` : operationId;
222
- const escapedFormattedOpId = formattedOpId.includes(',') ? `"${formattedOpId}"` : formattedOpId;
223
- const escapedTags = tagsStr.includes(',') ? `"${tagsStr}"` : tagsStr;
224
- const escapedFormattedTags = formattedTags.includes(',') ? `"${formattedTags}"` : formattedTags;
236
+ // Get operation description
237
+ const opDescription = operation.summary || operation.description || '';
238
+
239
+ // Escape fields that might contain commas, quotes, or other special characters
240
+ const escapedFields = {
241
+ filename: escapeCsvField(filename),
242
+ path: escapeCsvField(pathKey),
243
+ operationId: escapeCsvField(operationId),
244
+ formattedOpId: escapeCsvField(formattedOpId),
245
+ verb: escapeCsvField(verb),
246
+ responseRef: escapeCsvField(responseRef),
247
+ tagsStr: escapeCsvField(tagsStr),
248
+ formattedTags: escapeCsvField(formattedTags),
249
+ resourceName: escapeCsvField(resourceName),
250
+ methodName: escapeCsvField(methodName),
251
+ sqlVerb: escapeCsvField(sqlVerb),
252
+ opDescription: escapeCsvField(opDescription)
253
+ };
225
254
 
226
255
  // Write row
227
- writer.write(`${filename},${escapedPath},${escapedOperationId},${escapedFormattedOpId},${verb},${responseRef},${escapedTags},${escapedFormattedTags},${resourceName},${methodName},${sqlVerb}\n`);
256
+ writer.write(`${escapedFields.filename},${escapedFields.path},${escapedFields.operationId},${escapedFields.formattedOpId},${escapedFields.verb},${escapedFields.responseRef},${escapedFields.tagsStr},${escapedFields.formattedTags},${escapedFields.resourceName},${escapedFields.methodName},${escapedFields.sqlVerb},${escapedFields.opDescription}\n`);
228
257
  }
229
258
  }
230
259
  }
@@ -11,7 +11,6 @@ import {
11
11
 
12
12
  // Constants
13
13
  const OPERATIONS = ["get", "post", "put", "delete", "patch", "options", "head", "trace"];
14
- const NON_OPERATIONS = ["parameters", "servers", "summary", "description"];
15
14
  const COMPONENTS_CHILDREN = ["schemas", "responses", "parameters", "examples", "requestBodies", "headers", "securitySchemes", "links", "callbacks"];
16
15
 
17
16
  /**
@@ -42,9 +41,10 @@ function isOperationExcluded(exclude, opItem) {
42
41
  * @param {string} svcDiscriminator - Service discriminator
43
42
  * @param {Object[]} allTags - All tags from API doc
44
43
  * @param {boolean} debug - Debug flag
44
+ * @param {Object} svcNameOverrides - Service name overrides
45
45
  * @returns {[string, string]} - [service name, service description]
46
46
  */
47
- function retServiceNameAndDesc(providerName, opItem, pathKey, svcDiscriminator, allTags, debug) {
47
+ function retServiceNameAndDesc(providerName, opItem, pathKey, svcDiscriminator, allTags, debug, svcNameOverrides) {
48
48
  let service = "default";
49
49
  let serviceDesc = `${providerName} API`;
50
50
 
@@ -84,9 +84,25 @@ function retServiceNameAndDesc(providerName, opItem, pathKey, svcDiscriminator,
84
84
  return ["skip", ""];
85
85
  }
86
86
 
87
+ // Apply service name overrides if present
88
+ if (svcNameOverrides && svcNameOverrides[service]) {
89
+ const newName = svcNameOverrides[service];
90
+ if (debug) {
91
+ logger.debug(`Overriding service name: ${service} -> ${newName}`);
92
+ }
93
+
94
+ // Update service description for path-based services
95
+ if (svcDiscriminator === "path") {
96
+ serviceDesc = `${providerName} ${newName} API`;
97
+ }
98
+
99
+ service = newName;
100
+ }
101
+
87
102
  return [service, serviceDesc];
88
103
  }
89
104
 
105
+
90
106
  /**
91
107
  * Initialize service map
92
108
  * @param {Object} services - Services map
@@ -192,43 +208,6 @@ function getPathLevelRefs(pathItem) {
192
208
  return refs;
193
209
  }
194
210
 
195
- /**
196
- * Add referenced components to service
197
- * @param {Set<string>} refs - Set of refs
198
- * @param {Object} service - Service object
199
- * @param {Object} components - Components from API doc
200
- * @param {boolean} debug - Debug flag
201
- */
202
- function addRefsToComponents(refs, service, components, debug) {
203
- for (const ref of refs) {
204
- const parts = ref.split('/');
205
-
206
- // Only process refs that point to components
207
- if (parts.length >= 4 && parts[1] === "components") {
208
- const componentType = parts[2];
209
- const componentName = parts[3];
210
-
211
- // Check if component type exists in service
212
- if (!service.components[componentType]) {
213
- service.components[componentType] = {};
214
- }
215
-
216
- // Skip if component already added
217
- if (service.components[componentType][componentName]) {
218
- continue;
219
- }
220
-
221
- // Add component if it exists in source document
222
- if (components[componentType] && components[componentType][componentName]) {
223
- service.components[componentType][componentName] = components[componentType][componentName];
224
- if (debug) {
225
- logger.debug(`Added component ${componentType}/${componentName}`);
226
- }
227
- }
228
- }
229
- }
230
- }
231
-
232
211
  /**
233
212
  * Add missing type: object to schema objects
234
213
  * @param {any} obj - Object to process
@@ -338,7 +317,8 @@ export async function split(options) {
338
317
  svcDiscriminator = "tag",
339
318
  exclude = null,
340
319
  overwrite = true,
341
- verbose = false
320
+ verbose = false,
321
+ svcNameOverrides = {} // Add this new parameter with default empty object
342
322
  } = options;
343
323
 
344
324
  // Setup logging based on verbosity
@@ -351,6 +331,13 @@ export async function split(options) {
351
331
  logger.info(`Output: ${outputDir}`);
352
332
  logger.info(`Service Discriminator: ${svcDiscriminator}`);
353
333
 
334
+ if (Object.keys(svcNameOverrides).length > 0) {
335
+ logger.info(`Using ${Object.keys(svcNameOverrides).length} service name overrides`);
336
+ if (verbose) {
337
+ logger.debug(`Service name overrides: ${JSON.stringify(svcNameOverrides, null, 2)}`);
338
+ }
339
+ }
340
+
354
341
  // Process exclude list
355
342
  const excludeList = exclude ? exclude.split(",") : [];
356
343
 
@@ -409,12 +396,11 @@ export async function split(options) {
409
396
  continue;
410
397
  }
411
398
 
412
- // Determine service name
413
399
  const [service, serviceDesc] = retServiceNameAndDesc(
414
400
  providerName, opItem, pathKey, svcDiscriminator,
415
- apiDocObj.tags || [], verbose
416
- );
417
-
401
+ apiDocObj.tags || [], verbose, svcNameOverrides
402
+ );
403
+
418
404
  // Skip if service is marked to skip
419
405
  if (service === 'skip') {
420
406
  logger.warn(`⭐️ Skipping service: ${service}`);