@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 +2 -1
- package/package.json +1 -1
- package/src/docgen/resource/methods.js +28 -4
- package/src/providerdev/analyze.js +37 -8
- package/src/providerdev/split.js +30 -44
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# StackQL Provider Utils
|
|
2
2
|
|
|
3
|
-

|
|
3
|
+

|
|
4
|
+

|
|
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
|
@@ -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
|
|
42
|
-
|
|
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
|
-
//
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
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},${
|
|
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
|
}
|
package/src/providerdev/split.js
CHANGED
|
@@ -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}`);
|