@stackql/provider-utils 0.4.2 → 0.4.3
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 +1 -1
- package/src/providerdev/analyze.js +130 -6
- package/src/providerdev/generate.js +77 -4
package/package.json
CHANGED
|
@@ -51,6 +51,108 @@ function extractMain2xxResponse(responseObj) {
|
|
|
51
51
|
return '';
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Detect if an object should have an objectKey and determine what it should be
|
|
56
|
+
* @param {Object} spec - Full OpenAPI spec
|
|
57
|
+
* @param {Object} operation - Operation object
|
|
58
|
+
* @returns {string} - Suggested objectKey or empty string if none
|
|
59
|
+
*/
|
|
60
|
+
function detectObjectKey(spec, operation) {
|
|
61
|
+
// Only applicable for GET operations
|
|
62
|
+
if (!operation || !operation.responses) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let responseObject = null;
|
|
67
|
+
let responseCode = null;
|
|
68
|
+
|
|
69
|
+
// Find the first 2xx response
|
|
70
|
+
for (const [code, response] of Object.entries(operation.responses)) {
|
|
71
|
+
if (code.startsWith('2')) {
|
|
72
|
+
responseCode = code;
|
|
73
|
+
// Handle direct response or reference
|
|
74
|
+
if (response.$ref) {
|
|
75
|
+
// Resolve reference
|
|
76
|
+
const refParts = response.$ref.split('/');
|
|
77
|
+
const componentType = refParts[2];
|
|
78
|
+
const responseName = refParts[3];
|
|
79
|
+
responseObject = spec.components?.[componentType]?.[responseName];
|
|
80
|
+
} else {
|
|
81
|
+
responseObject = response;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!responseObject) {
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Get the schema from the response
|
|
92
|
+
let schema = null;
|
|
93
|
+
|
|
94
|
+
// If it's a direct response object
|
|
95
|
+
if (responseObject.content?.['application/json']?.schema) {
|
|
96
|
+
schema = responseObject.content['application/json'].schema;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If the schema is a reference, resolve it
|
|
100
|
+
if (schema && schema.$ref) {
|
|
101
|
+
const refParts = schema.$ref.split('/');
|
|
102
|
+
const componentType = refParts[2];
|
|
103
|
+
const schemaName = refParts[3];
|
|
104
|
+
schema = spec.components?.[componentType]?.[schemaName];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle allOf case (like in the droplets_list example)
|
|
108
|
+
if (schema && schema.allOf) {
|
|
109
|
+
// Look at the first object in allOf that has properties
|
|
110
|
+
for (const subSchema of schema.allOf) {
|
|
111
|
+
if (subSchema.properties && Object.keys(subSchema.properties).length === 1) {
|
|
112
|
+
const key = Object.keys(subSchema.properties)[0];
|
|
113
|
+
return `$.${key}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle direct properties case (like in the droplets_get example)
|
|
119
|
+
if (schema && schema.properties) {
|
|
120
|
+
// If there's only one property at the top level, and it's not a primitive
|
|
121
|
+
const propKeys = Object.keys(schema.properties);
|
|
122
|
+
if (propKeys.length === 1) {
|
|
123
|
+
const key = propKeys[0];
|
|
124
|
+
const prop = schema.properties[key];
|
|
125
|
+
|
|
126
|
+
// Check if the property is an object or array, not a primitive
|
|
127
|
+
if (prop.$ref ||
|
|
128
|
+
prop.type === 'object' ||
|
|
129
|
+
prop.type === 'array' ||
|
|
130
|
+
(prop.properties && Object.keys(prop.properties).length > 0)) {
|
|
131
|
+
return `$.${key}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Map HTTP verb to SQL verb
|
|
141
|
+
* @param {string} httpVerb - HTTP verb (get, post, put, etc)
|
|
142
|
+
* @returns {string} - Corresponding SQL verb
|
|
143
|
+
*/
|
|
144
|
+
function mapToSqlVerb(httpVerb) {
|
|
145
|
+
const verbMap = {
|
|
146
|
+
'get': 'select',
|
|
147
|
+
'post': 'insert',
|
|
148
|
+
'delete': 'delete',
|
|
149
|
+
'put': 'replace',
|
|
150
|
+
'patch': 'update'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return verbMap[httpVerb] || 'exec';
|
|
154
|
+
}
|
|
155
|
+
|
|
54
156
|
/**
|
|
55
157
|
* Find existing mapping in x-stackQL-resources
|
|
56
158
|
* @param {Object} spec - OpenAPI spec
|
|
@@ -77,10 +179,14 @@ function findExistingMapping(spec, pathRef) {
|
|
|
77
179
|
}
|
|
78
180
|
}
|
|
79
181
|
|
|
182
|
+
// Get objectKey if present
|
|
183
|
+
const objectKey = method.response?.objectKey || '';
|
|
184
|
+
|
|
80
185
|
return {
|
|
81
186
|
resourceName,
|
|
82
187
|
methodName,
|
|
83
|
-
sqlVerb
|
|
188
|
+
sqlVerb,
|
|
189
|
+
objectKey
|
|
84
190
|
};
|
|
85
191
|
}
|
|
86
192
|
}
|
|
@@ -91,7 +197,8 @@ function findExistingMapping(spec, pathRef) {
|
|
|
91
197
|
return {
|
|
92
198
|
resourceName: '',
|
|
93
199
|
methodName: '',
|
|
94
|
-
sqlVerb: ''
|
|
200
|
+
sqlVerb: '',
|
|
201
|
+
objectKey: ''
|
|
95
202
|
};
|
|
96
203
|
}
|
|
97
204
|
|
|
@@ -150,7 +257,8 @@ export async function analyze(options) {
|
|
|
150
257
|
existingMappings[key] = {
|
|
151
258
|
resourceName: row.stackql_resource_name || '',
|
|
152
259
|
methodName: row.stackql_method_name || '',
|
|
153
|
-
sqlVerb: row.stackql_verb || ''
|
|
260
|
+
sqlVerb: row.stackql_verb || '',
|
|
261
|
+
objectKey: row.stackql_object_key || ''
|
|
154
262
|
};
|
|
155
263
|
}
|
|
156
264
|
})
|
|
@@ -176,7 +284,7 @@ export async function analyze(options) {
|
|
|
176
284
|
|
|
177
285
|
// Only write header if creating a new file
|
|
178
286
|
if (!fileExists) {
|
|
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');
|
|
287
|
+
writer.write('filename,path,operationId,formatted_op_id,verb,response_object,tags,formatted_tags,stackql_resource_name,stackql_method_name,stackql_verb,stackql_object_key,op_description\n');
|
|
180
288
|
}
|
|
181
289
|
|
|
182
290
|
const files = fs.readdirSync(inputDir);
|
|
@@ -231,7 +339,22 @@ export async function analyze(options) {
|
|
|
231
339
|
const pathRef = `#/paths/${encodedPath}/${verb}`;
|
|
232
340
|
|
|
233
341
|
// Find existing mapping if available
|
|
234
|
-
|
|
342
|
+
let { resourceName, methodName, sqlVerb, objectKey } = findExistingMapping(spec, pathRef);
|
|
343
|
+
|
|
344
|
+
// CHANGE 1: Default methodName to formattedOpId if not found
|
|
345
|
+
if (!methodName) {
|
|
346
|
+
methodName = formattedOpId;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// CHANGE 2: Default sqlVerb based on HTTP verb if not found
|
|
350
|
+
if (!sqlVerb) {
|
|
351
|
+
sqlVerb = mapToSqlVerb(verb);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// CHANGE 3: Detect and set objectKey for GET operations if not already set
|
|
355
|
+
if (!objectKey && verb === 'get') {
|
|
356
|
+
objectKey = detectObjectKey(spec, operation);
|
|
357
|
+
}
|
|
235
358
|
|
|
236
359
|
// Get operation description
|
|
237
360
|
const opDescription = operation.summary || operation.description || '';
|
|
@@ -249,11 +372,12 @@ export async function analyze(options) {
|
|
|
249
372
|
resourceName: escapeCsvField(resourceName),
|
|
250
373
|
methodName: escapeCsvField(methodName),
|
|
251
374
|
sqlVerb: escapeCsvField(sqlVerb),
|
|
375
|
+
objectKey: escapeCsvField(objectKey),
|
|
252
376
|
opDescription: escapeCsvField(opDescription)
|
|
253
377
|
};
|
|
254
378
|
|
|
255
379
|
// Write row
|
|
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`);
|
|
380
|
+
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.objectKey},${escapedFields.opDescription}\n`);
|
|
257
381
|
}
|
|
258
382
|
}
|
|
259
383
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/providerdev/generate.js
|
|
2
1
|
import fs from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import yaml from 'js-yaml';
|
|
@@ -75,14 +74,15 @@ function getSuccessResponseInfo(operation) {
|
|
|
75
74
|
.sort();
|
|
76
75
|
|
|
77
76
|
if (twoXxCodes.length === 0) {
|
|
78
|
-
|
|
77
|
+
throw new Error('No 2xx response found, openAPIDocKey is required');
|
|
79
78
|
}
|
|
80
79
|
|
|
81
80
|
const lowest2xx = twoXxCodes[0];
|
|
82
81
|
const content = responses[lowest2xx]?.content || {};
|
|
83
82
|
const mediaTypes = Object.keys(content);
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
// Default to 'application/json' if mediaType is not found
|
|
85
|
+
const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : 'application/json';
|
|
86
86
|
|
|
87
87
|
return {
|
|
88
88
|
mediaType,
|
|
@@ -99,6 +99,71 @@ function snakeCase(name) {
|
|
|
99
99
|
return name.replace(/-/g, '_');
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Count the number of path parameters in a path
|
|
104
|
+
* @param {string} path - HTTP path
|
|
105
|
+
* @returns {number} - Number of path parameters
|
|
106
|
+
*/
|
|
107
|
+
function countPathParams(path) {
|
|
108
|
+
// Match all path parameters like {param_name}
|
|
109
|
+
const matches = path.match(/\{[^}]+\}/g);
|
|
110
|
+
return matches ? matches.length : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sort operations from most specific to least specific based on path parameters
|
|
115
|
+
* @param {Object} resources - Resources object containing methods and sqlVerbs
|
|
116
|
+
* @param {Object} spec - Full OpenAPI specification
|
|
117
|
+
* @returns {Object} - Resources with sorted sqlVerbs
|
|
118
|
+
*/
|
|
119
|
+
function sortOperationsBySpecificity(resources, spec) {
|
|
120
|
+
// For each resource
|
|
121
|
+
for (const resourceName in resources) {
|
|
122
|
+
const resource = resources[resourceName];
|
|
123
|
+
const methods = resource.methods;
|
|
124
|
+
|
|
125
|
+
// Create a map of method references to their specificity (path param count)
|
|
126
|
+
const methodSpecificityMap = {};
|
|
127
|
+
|
|
128
|
+
// For each method, find its operation ref and count path params
|
|
129
|
+
for (const methodName in methods) {
|
|
130
|
+
const method = methods[methodName];
|
|
131
|
+
const operationRef = method.operation.$ref;
|
|
132
|
+
|
|
133
|
+
// Extract path and verb from the reference
|
|
134
|
+
// Reference format: '#/paths/{encodedPath}/{verb}'
|
|
135
|
+
const refParts = operationRef.split('/');
|
|
136
|
+
const verb = refParts.pop();
|
|
137
|
+
// Remove '#/paths/' and the verb, then decode the path
|
|
138
|
+
const encodedPath = refParts.slice(2).join('/');
|
|
139
|
+
const path = encodedPath.replace(/~1/g, '/');
|
|
140
|
+
|
|
141
|
+
// Count path parameters
|
|
142
|
+
const paramCount = countPathParams(path);
|
|
143
|
+
|
|
144
|
+
// Store the method reference and its path parameter count
|
|
145
|
+
const methodRef = `#/components/x-stackQL-resources/${resourceName}/methods/${methodName}`;
|
|
146
|
+
methodSpecificityMap[methodRef] = paramCount;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// For each SQL verb, sort the operations by specificity
|
|
150
|
+
for (const verbName in resource.sqlVerbs) {
|
|
151
|
+
const operations = resource.sqlVerbs[verbName];
|
|
152
|
+
|
|
153
|
+
if (operations && operations.length > 0) {
|
|
154
|
+
// Sort operations from most specific (more path params) to least specific
|
|
155
|
+
operations.sort((a, b) => {
|
|
156
|
+
const aRef = a.$ref;
|
|
157
|
+
const bRef = b.$ref;
|
|
158
|
+
return methodSpecificityMap[bRef] - methodSpecificityMap[aRef];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return resources;
|
|
165
|
+
}
|
|
166
|
+
|
|
102
167
|
/**
|
|
103
168
|
* Generate StackQL provider extensions
|
|
104
169
|
* @param {Object} options - Options for generation
|
|
@@ -234,6 +299,11 @@ export async function generate(options) {
|
|
|
234
299
|
response: responseInfo
|
|
235
300
|
};
|
|
236
301
|
|
|
302
|
+
// Add objectKey to the response info if it exists in the manifest and is for a GET operation
|
|
303
|
+
if (entry.stackql_object_key && verb === 'get') {
|
|
304
|
+
methodEntry.response.objectKey = entry.stackql_object_key;
|
|
305
|
+
}
|
|
306
|
+
|
|
237
307
|
resources[resource].methods[method] = methodEntry;
|
|
238
308
|
if (sqlverb && sqlverb === 'exec') {
|
|
239
309
|
logger.info(`exec method skipped: ${resource}.${method}`);
|
|
@@ -247,11 +317,14 @@ export async function generate(options) {
|
|
|
247
317
|
}
|
|
248
318
|
}
|
|
249
319
|
|
|
320
|
+
// Sort operations by specificity before injecting into spec
|
|
321
|
+
const sortedResources = sortOperationsBySpecificity(resources, spec);
|
|
322
|
+
|
|
250
323
|
// Inject into spec
|
|
251
324
|
if (!spec.components) {
|
|
252
325
|
spec.components = {};
|
|
253
326
|
}
|
|
254
|
-
spec.components['x-stackQL-resources'] =
|
|
327
|
+
spec.components['x-stackQL-resources'] = sortedResources;
|
|
255
328
|
|
|
256
329
|
// Inject servers if provided
|
|
257
330
|
if (servers) {
|