@stackql/provider-utils 0.5.7 → 0.5.8

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.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -159,6 +159,7 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
159
159
  optionalParams: optionalParams || {},
160
160
  requestBody: requestBody || {},
161
161
  rawRespProps: respProps,
162
+ methodConfig: methodData.config || null,
162
163
  };
163
164
 
164
165
  // Format and sort the properties using our helper functions
@@ -177,7 +178,7 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
177
178
  }
178
179
 
179
180
  for (const thisMethod of resourceData.sqlVerbs[sqlVerb]) {
180
- const {path, httpVerb, mediaType, openAPIDocKey, objectKey, methodName} = getHttpOperationForSqlVerb(thisMethod.$ref, resourceData);
181
+ const {path, httpVerb, mediaType, openAPIDocKey, objectKey, methodName, methodConfig} = getHttpOperationForSqlVerb(thisMethod.$ref, resourceData);
181
182
  const {respProps, respDescription, opDescription, opSummary, requestBody} = getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAPIDocKey, objectKey);
182
183
  const {requiredParams, optionalParams} = getHttpOperationParams(dereferencedAPI, path, httpVerb);
183
184
 
@@ -191,6 +192,7 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
191
192
  optionalParams: optionalParams || {},
192
193
  requestBody: requestBody || {},
193
194
  rawRespProps: respProps,
195
+ methodConfig: methodConfig || null,
194
196
  };
195
197
 
196
198
  // Format and sort the properties using our helper functions
@@ -255,12 +257,19 @@ function formatProperties(respProps) {
255
257
  // Get the base description
256
258
  let fullDescription = propDetails.description || '';
257
259
  fullDescription = fullDescription.replace(/\n/g, ' ');
260
+
261
+ // Collect enum values from enum or x-enum keys
262
+ const enumValues = propDetails['enum'] || propDetails['x-enum'];
263
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
264
+ fullDescription += ` (${enumValues.join(', ')})`;
265
+ }
266
+
258
267
  let additionalDescriptionPaths = [];
259
268
 
260
269
  // Add all other fields to description parts
261
270
  for (const [fieldName, fieldValue] of Object.entries(propDetails)) {
262
271
  // Skip the fields we're handling separately
263
- if (fieldName === 'type' || fieldName === 'format' || fieldName === 'description') {
272
+ if (fieldName === 'type' || fieldName === 'format' || fieldName === 'description' || fieldName === 'enum' || fieldName === 'x-enum') {
264
273
  continue;
265
274
  }
266
275
 
@@ -362,13 +371,14 @@ function getHttpOperationForSqlVerb(sqlVerbRef, resourceData){
362
371
  const httpVerb = operationRef.split('/').pop()
363
372
  const path = operationRef.split('/')[0].replaceAll('~1','/');
364
373
 
365
- return {
366
- path,
367
- httpVerb,
368
- mediaType: methodObj.response.mediaType,
374
+ return {
375
+ path,
376
+ httpVerb,
377
+ mediaType: methodObj.response.mediaType,
369
378
  openAPIDocKey: methodObj.response.openAPIDocKey,
370
379
  objectKey: methodObj.response.objectKey || false,
371
- methodName
380
+ methodName,
381
+ methodConfig: methodObj.config || null
372
382
  }
373
383
  }
374
384
 
@@ -595,6 +605,12 @@ export function generateSchemaJsonFromProps(respProps, depth = 0, maxDepth = 4)
595
605
  let propType = prop.type || 'object';
596
606
  let propDesc = sanitizeHtml(prop.description || '');
597
607
 
608
+ // Collect enum values from enum or x-enum keys
609
+ const enumValues = prop['enum'] || prop['x-enum'];
610
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
611
+ propDesc += ` (${enumValues.join(', ')})`;
612
+ }
613
+
598
614
  // Add format info to type string if available
599
615
  if (prop.format) {
600
616
  propType += ` (${prop.format})`;
@@ -40,16 +40,21 @@ export function createInsertExamples(providerName, serviceName, resourceName, re
40
40
  // Create SQL example
41
41
  content += '\n\n```sql\nINSERT INTO ' + providerName + '.' + serviceName + '.' + resourceName + ' (\n';
42
42
 
43
- // Add requestBody fields prefixed with data__ (excluding read-only props)
44
- const reqBodyProps = methodDetails.requestBody?.properties
43
+ // Add requestBody fields (excluding read-only props)
44
+ // If requestBodyTranslate.algorithm is 'naive', do not prefix with data__
45
+ const hasNaiveTranslate = methodDetails.methodConfig?.requestBodyTranslate?.algorithm === 'naive';
46
+
47
+ const reqBodyProps = methodDetails.requestBody?.properties
45
48
  ? Object.entries(methodDetails.requestBody.properties)
46
49
  .filter(([_, propDetails]) => propDetails.readOnly !== true)
47
50
  .map(([propName]) => propName)
48
51
  : [];
49
52
 
50
53
  const requiredBodyProps = methodDetails.requestBody?.required ? methodDetails.requestBody.required : [];
51
-
52
- const dataProps = reqBodyProps.map(prop => 'data__' + prop);
54
+
55
+ const dataProps = hasNaiveTranslate
56
+ ? reqBodyProps
57
+ : reqBodyProps.map(prop => 'data__' + prop);
53
58
 
54
59
  // Combine data props with params
55
60
  const requiredParams = Object.keys(methodDetails.requiredParams || {});
@@ -62,11 +67,18 @@ export function createInsertExamples(providerName, serviceName, resourceName, re
62
67
  // Start SELECT statement
63
68
  content += '\n)\nSELECT \n';
64
69
 
70
+ // Build a set of body prop names for quick lookup
71
+ const bodyPropSet = new Set(reqBodyProps);
72
+
65
73
  // Add values placeholders
66
74
  const valueLines = allFields.map(field => {
67
- const isDataField = field.startsWith('data__');
68
- const paramName = isDataField ? field.substring(6) : field;
69
-
75
+ const isDataField = hasNaiveTranslate
76
+ ? bodyPropSet.has(field)
77
+ : field.startsWith('data__');
78
+ const paramName = isDataField
79
+ ? (hasNaiveTranslate ? field : field.substring(6))
80
+ : field;
81
+
70
82
  // Check for required body props
71
83
  let isRequiredBodyParam = false;
72
84
  if(isDataField){
@@ -78,13 +90,13 @@ export function createInsertExamples(providerName, serviceName, resourceName, re
78
90
  // Check if it's a number or boolean type
79
91
  let isNumber = false;
80
92
  let isBoolean = false;
81
-
93
+
82
94
  if (isDataField && methodDetails.requestBody?.properties?.[paramName]) {
83
95
  const propType = methodDetails.requestBody.properties[paramName].type;
84
96
  isNumber = propType === 'number' || propType === 'integer';
85
97
  isBoolean = propType === 'boolean';
86
98
  }
87
-
99
+
88
100
  if (isNumber || isBoolean) {
89
101
  return '{{ ' + paramName + ' }}' + (isRequiredBodyParam ? ' /* required */' : '');
90
102
  } else {
@@ -42,95 +42,100 @@ export function createUpdateExamples(providerName, serviceName, resourceName, re
42
42
  content += '\n\n```sql\n';
43
43
  content += (isReplace ? 'REPLACE ' : 'UPDATE ') + providerName + '.' + serviceName + '.' + resourceName;
44
44
 
45
+ // If requestBodyTranslate.algorithm is 'naive', do not prefix with data__
46
+ const hasNaiveTranslate = methodDetails.methodConfig?.requestBodyTranslate?.algorithm === 'naive';
47
+
45
48
  // Add SET clause with requestBody fields (excluding read-only props)
46
49
  content += '\nSET \n';
47
-
50
+
48
51
  // Get request body fields (excluding read-only props)
49
- const reqBodyProps = methodDetails.requestBody?.properties
52
+ const reqBodyProps = methodDetails.requestBody?.properties
50
53
  ? Object.entries(methodDetails.requestBody.properties)
51
54
  .filter(([_, propDetails]) => propDetails.readOnly !== true)
52
55
  .map(([propName]) => propName)
53
56
  : [];
54
-
55
- // Add data__ prefixed fields to SET clause
57
+
58
+ // Add fields to SET clause (with or without data__ prefix)
56
59
  if (reqBodyProps.length > 0) {
57
60
  const setLines = reqBodyProps.map(prop => {
58
61
  const propDetails = methodDetails.requestBody.properties[prop];
59
62
  const isNumber = propDetails.type === 'number' || propDetails.type === 'integer';
60
63
  const isBoolean = propDetails.type === 'boolean';
61
-
64
+ const fieldName = hasNaiveTranslate ? prop : 'data__' + prop;
65
+
62
66
  if (isNumber || isBoolean) {
63
- return 'data__' + prop + ' = {{ ' + prop + ' }}';
67
+ return fieldName + ' = {{ ' + prop + ' }}';
64
68
  } else {
65
- return 'data__' + prop + ' = \'{{ ' + prop + ' }}\'';
69
+ return fieldName + ' = \'{{ ' + prop + ' }}\'';
66
70
  }
67
71
  });
68
-
72
+
69
73
  content += setLines.join(',\n');
70
74
  } else {
71
75
  content += '-- No updatable properties';
72
76
  }
73
-
77
+
74
78
  // Add WHERE clause with parameters
75
79
  const requiredParams = Object.keys(methodDetails.requiredParams || {});
76
80
  const optionalParams = Object.keys(methodDetails.optionalParams || {});
77
-
81
+
78
82
  // Get required body props (excluding read-only props)
79
- const requiredBodyProps = methodDetails.requestBody?.required
80
- ? methodDetails.requestBody.required.filter(prop =>
81
- methodDetails.requestBody.properties[prop] &&
83
+ const requiredBodyProps = methodDetails.requestBody?.required
84
+ ? methodDetails.requestBody.required.filter(prop =>
85
+ methodDetails.requestBody.properties[prop] &&
82
86
  methodDetails.requestBody.properties[prop].readOnly !== true)
83
87
  : [];
84
-
88
+
85
89
  if (requiredParams.length > 0 || requiredBodyProps.length > 0 || optionalParams.length > 0) {
86
90
 
87
91
  content += '\nWHERE \n';
88
-
92
+
89
93
  // Add required parameters
90
94
  let clauseCount = 0;
91
-
95
+
92
96
  // Add required query/path/header params
93
97
  requiredParams.forEach(param => {
94
98
  if (clauseCount > 0) content += '\nAND ';
95
99
  content += param + ' = \'{{ ' + param + ' }}\' --required';
96
100
  clauseCount++;
97
101
  });
98
-
102
+
99
103
  // Add required body params (only non-readonly ones)
100
104
  requiredBodyProps.forEach(prop => {
101
105
  if (clauseCount > 0) content += '\nAND ';
102
-
106
+
103
107
  const propDetails = methodDetails.requestBody.properties[prop];
104
108
  const isBoolean = propDetails.type === 'boolean';
105
-
109
+ const fieldName = hasNaiveTranslate ? prop : 'data__' + prop;
110
+
106
111
  if (isBoolean) {
107
- content += 'data__' + prop + ' = {{ ' + prop + ' }} --required';
112
+ content += fieldName + ' = {{ ' + prop + ' }} --required';
108
113
  } else {
109
- content += 'data__' + prop + ' = \'{{ ' + prop + ' }}\' --required';
114
+ content += fieldName + ' = \'{{ ' + prop + ' }}\' --required';
110
115
  }
111
-
116
+
112
117
  clauseCount++;
113
118
  });
114
-
119
+
115
120
  // Add optional parameters
116
121
  optionalParams.forEach(param => {
117
122
  if (clauseCount > 0) content += '\nAND ';
118
-
123
+
119
124
  // For boolean parameters, we can add a comment about their default value
120
125
  const paramDetails = methodDetails.optionalParams[param];
121
126
  const isBoolean = paramDetails.type === 'boolean';
122
127
  const defaultValue = paramDetails.default;
123
-
128
+
124
129
  if (isBoolean) {
125
130
  content += param + ' = {{ ' + param + '}}';
126
131
  } else {
127
132
  content += param + ' = \'{{ ' + param + '}}\'';
128
133
  }
129
-
134
+
130
135
  if (isBoolean && defaultValue !== undefined) {
131
136
  content += ' -- default: ' + defaultValue;
132
137
  }
133
-
138
+
134
139
  clauseCount++;
135
140
  });
136
141
  }
@@ -9,15 +9,18 @@ const getRequiredBodyParams = (methodDetails, accessType) => {
9
9
  if (!['insert', 'update', 'replace', 'exec'].includes(accessType)) {
10
10
  return [];
11
11
  }
12
-
12
+
13
13
  // Get required body params if they exist
14
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)) {
15
+
16
+ // Check if requestBodyTranslate.algorithm is 'naive' - if so, don't prefix with data__
17
+ const hasNaiveTranslate = methodDetails.methodConfig?.requestBodyTranslate?.algorithm === 'naive';
18
+
19
+ // For insert, update, and replace, prefix with data__ (unless naive translate is set)
20
+ if (['insert', 'update', 'replace'].includes(accessType) && !hasNaiveTranslate) {
18
21
  return requiredBodyProps.map(prop => `data__${prop}`);
19
22
  } else {
20
- // For exec, don't prefix
23
+ // For exec or naive translate, don't prefix
21
24
  return requiredBodyProps;
22
25
  }
23
26
  };
@@ -177,6 +177,8 @@ export async function generate(options) {
177
177
  providerId,
178
178
  servers = null,
179
179
  providerConfig = null,
180
+ serviceConfig = null,
181
+ naiveReqBodyTranslate = false,
180
182
  skipFiles = []
181
183
  } = options;
182
184
 
@@ -217,8 +219,19 @@ export async function generate(options) {
217
219
  return false;
218
220
  }
219
221
 
222
+ // Parse serviceConfig if provided
223
+ let serviceConfigJson = null;
224
+ if (serviceConfig) {
225
+ try {
226
+ serviceConfigJson = JSON.parse(serviceConfig);
227
+ } catch (error) {
228
+ logger.error(`❌ Failed to parse service config JSON: ${error.message}`);
229
+ return false;
230
+ }
231
+ }
232
+
220
233
  const providerServices = {};
221
-
234
+
222
235
  try {
223
236
  const files = fs.readdirSync(inputDir);
224
237
 
@@ -302,10 +315,22 @@ export async function generate(options) {
302
315
  const pathRef = encodeRefPath(pathKey, verb);
303
316
  const responseInfo = getSuccessResponseInfo(operation);
304
317
 
305
- const methodEntry = {
306
- operation: { $ref: pathRef },
307
- response: responseInfo
308
- };
318
+ const methodEntry = {};
319
+
320
+ // Add requestBodyTranslate config for methods with request bodies
321
+ if (naiveReqBodyTranslate && ['post', 'put', 'patch'].includes(verb)) {
322
+ // Check if the operation actually has a request body
323
+ if (operation.requestBody) {
324
+ methodEntry.config = {
325
+ requestBodyTranslate: {
326
+ algorithm: 'naive'
327
+ }
328
+ };
329
+ }
330
+ }
331
+
332
+ methodEntry.operation = { $ref: pathRef };
333
+ methodEntry.response = responseInfo;
309
334
 
310
335
  // Add objectKey to the response info if it exists in the manifest and is for a GET operation
311
336
  if (entry.stackql_object_key && verb === 'get') {
@@ -344,7 +369,12 @@ export async function generate(options) {
344
369
  return false;
345
370
  }
346
371
  }
347
-
372
+
373
+ // Inject x-stackQL-config if serviceConfig is provided
374
+ if (serviceConfigJson) {
375
+ spec['x-stackQL-config'] = serviceConfigJson;
376
+ }
377
+
348
378
  // Write enriched spec (always as YAML, ensuring .yaml extension)
349
379
  const outputFilename = filename.endsWith('.json')
350
380
  ? filename.replace(/\.json$/, '.yaml')