@stackql/provider-utils 0.5.5 → 0.5.7
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/docgen/generator.js +219 -1
- package/src/docgen/helpers.js +110 -27
- package/src/docgen/index.js +2 -2
- package/src/docgen/resource/examples/delete-example.js +2 -2
- package/src/docgen/resource/examples/exec-example.js +2 -2
- package/src/docgen/resource/examples/insert-example.js +2 -3
- package/src/docgen/resource/examples/select-example.js +2 -2
- package/src/docgen/resource/examples/update-example.js +2 -4
- package/src/docgen/resource/examples.js +10 -10
- package/src/docgen/resource/fields.js +63 -2
- package/src/docgen/resource/methods.js +2 -2
- package/src/docgen/resource/overview.js +40 -0
- package/src/docgen/resource-content.js +22 -5
package/package.json
CHANGED
package/src/docgen/generator.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
|
-
import { createResourceIndexContent } from './resource-content.js';
|
|
6
|
+
import { createResourceIndexContent, createResourceIndexContentv2 } from './resource-content.js';
|
|
7
7
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
8
8
|
import * as deno_openapi_dereferencer from "@stackql/deno-openapi-dereferencer";
|
|
9
9
|
|
|
@@ -284,6 +284,224 @@ function generateResourceLinks(providerName, serviceName, resources) {
|
|
|
284
284
|
return resourceLinks.join('<br />\n');
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
export async function generateDocsv2(options) {
|
|
288
|
+
const {
|
|
289
|
+
providerName,
|
|
290
|
+
providerDir, // e.g., 'output/src/heroku/v00.00.00000'
|
|
291
|
+
outputDir, // e.g., 'website'
|
|
292
|
+
providerDataDir, // e.g., 'config/provider-data'
|
|
293
|
+
dereferenced = false,
|
|
294
|
+
succinct = false, // use summary instead of description for method/example descriptions
|
|
295
|
+
} = options;
|
|
296
|
+
|
|
297
|
+
console.log(`documenting ${providerName} (v2)...`);
|
|
298
|
+
|
|
299
|
+
const docsDir = path.join(outputDir, `docs`);
|
|
300
|
+
const servicesDir = path.join(docsDir, `services`);
|
|
301
|
+
|
|
302
|
+
// Remove directory if it exists, then create it fresh
|
|
303
|
+
fs.existsSync(docsDir) && fs.rmSync(docsDir, { recursive: true, force: true });
|
|
304
|
+
fs.mkdirSync(servicesDir, { recursive: true });
|
|
305
|
+
|
|
306
|
+
// Check for provider data files
|
|
307
|
+
console.log(providerDataDir);
|
|
308
|
+
try {
|
|
309
|
+
const files = fs.readdirSync(providerDataDir);
|
|
310
|
+
console.log('Files in providerDataDir:', files);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.error('Error reading providerDataDir:', err.message);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const headerContent1Path = path.join(providerDataDir, 'headerContent1.txt');
|
|
316
|
+
const headerContent2Path = path.join(providerDataDir, 'headerContent2.txt');
|
|
317
|
+
|
|
318
|
+
if (!fs.existsSync(headerContent1Path) || !fs.existsSync(headerContent2Path)) {
|
|
319
|
+
throw new Error(`Missing headerContent1.txt or headerContent2.txt in ${providerDataDir}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const headerContent1 = fs.readFileSync(headerContent1Path, 'utf8');
|
|
323
|
+
const headerContent2 = fs.readFileSync(headerContent2Path, 'utf8');
|
|
324
|
+
|
|
325
|
+
// Initialize counters
|
|
326
|
+
let servicesForIndex = [];
|
|
327
|
+
let totalServicesCount = 0;
|
|
328
|
+
let totalResourcesCount = 0;
|
|
329
|
+
|
|
330
|
+
// Process services
|
|
331
|
+
const serviceDir = path.join(providerDir, 'services');
|
|
332
|
+
console.log(`Processing services in ${serviceDir}...`);
|
|
333
|
+
const serviceFiles = fs.readdirSync(serviceDir).filter(file => path.extname(file) === '.yaml');
|
|
334
|
+
|
|
335
|
+
for (const file of serviceFiles) {
|
|
336
|
+
const serviceName = path.basename(file, '.yaml').replace(/-/g, '_');
|
|
337
|
+
console.log(`Processing service: ${serviceName}`);
|
|
338
|
+
servicesForIndex.push(serviceName);
|
|
339
|
+
const filePath = path.join(serviceDir, file);
|
|
340
|
+
totalServicesCount++;
|
|
341
|
+
const serviceFolder = `${servicesDir}/${serviceName}`;
|
|
342
|
+
await createDocsForServicev2(filePath, providerName, serviceName, serviceFolder, dereferenced, succinct);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(`Processed ${totalServicesCount} services`);
|
|
346
|
+
|
|
347
|
+
// Count total resources
|
|
348
|
+
totalResourcesCount = fs.readdirSync(`${servicesDir}`, { withFileTypes: true })
|
|
349
|
+
.filter(dirent => dirent.isDirectory())
|
|
350
|
+
.map(dirent => fs.readdirSync(`${servicesDir}/${dirent.name}`).length)
|
|
351
|
+
.reduce((a, b) => a + b, 0);
|
|
352
|
+
|
|
353
|
+
console.log(`Processed ${totalResourcesCount} resources`);
|
|
354
|
+
|
|
355
|
+
// Create provider index
|
|
356
|
+
servicesForIndex = [...new Set(servicesForIndex)];
|
|
357
|
+
servicesForIndex.sort();
|
|
358
|
+
|
|
359
|
+
const half = Math.ceil(servicesForIndex.length / 2);
|
|
360
|
+
const firstColumnServices = servicesForIndex.slice(0, half);
|
|
361
|
+
const secondColumnServices = servicesForIndex.slice(half);
|
|
362
|
+
|
|
363
|
+
const indexContent = `${headerContent1}
|
|
364
|
+
|
|
365
|
+
:::info[Provider Summary]
|
|
366
|
+
|
|
367
|
+
total services: __${totalServicesCount}__
|
|
368
|
+
total resources: __${totalResourcesCount}__
|
|
369
|
+
|
|
370
|
+
:::
|
|
371
|
+
|
|
372
|
+
${headerContent2}
|
|
373
|
+
|
|
374
|
+
## Services
|
|
375
|
+
<div class="row">
|
|
376
|
+
<div class="providerDocColumn">
|
|
377
|
+
${servicesToMarkdown(providerName, firstColumnServices)}
|
|
378
|
+
</div>
|
|
379
|
+
<div class="providerDocColumn">
|
|
380
|
+
${servicesToMarkdown(providerName, secondColumnServices)}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
// Write index
|
|
386
|
+
const indexPath = path.join(docsDir, 'index.md');
|
|
387
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
388
|
+
console.log(`Index file created at ${indexPath}`);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
totalServices: totalServicesCount,
|
|
392
|
+
totalResources: totalResourcesCount,
|
|
393
|
+
outputPath: docsDir
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// v2 service processing - uses SchemaTable for collapsible nested fields
|
|
398
|
+
async function createDocsForServicev2(yamlFilePath, providerName, serviceName, serviceFolder, dereferenced = false, succinct = false) {
|
|
399
|
+
|
|
400
|
+
const data = yaml.load(fs.readFileSync(yamlFilePath, 'utf8'));
|
|
401
|
+
|
|
402
|
+
// Create a new SwaggerParser instance
|
|
403
|
+
let parser = new SwaggerParser();
|
|
404
|
+
const api = await parser.parse(yamlFilePath);
|
|
405
|
+
const ignorePaths = ["$.components.x-stackQL-resources"];
|
|
406
|
+
let dereferencedAPI;
|
|
407
|
+
|
|
408
|
+
if (dereferenced) {
|
|
409
|
+
// If API is already dereferenced, just use it as is
|
|
410
|
+
dereferencedAPI = api;
|
|
411
|
+
} else {
|
|
412
|
+
try {
|
|
413
|
+
// Only dereference and flatten if needed
|
|
414
|
+
dereferencedAPI = await SwaggerParser.dereference(api);
|
|
415
|
+
dereferencedAPI = await deno_openapi_dereferencer.flattenAllOf(dereferencedAPI);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error("error in dereferencing or flattening:", error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Create service directory
|
|
422
|
+
if (!fs.existsSync(serviceFolder)) {
|
|
423
|
+
fs.mkdirSync(serviceFolder, { recursive: true });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const resourcesObj = data.components['x-stackQL-resources'];
|
|
427
|
+
|
|
428
|
+
if (!resourcesObj) {
|
|
429
|
+
console.warn(`No resources found in ${yamlFilePath}`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const resources = [];
|
|
434
|
+
for (let resourceName in resourcesObj) {
|
|
435
|
+
|
|
436
|
+
let resourceData = resourcesObj[resourceName];
|
|
437
|
+
if (!resourceData.id) {
|
|
438
|
+
console.warn(`No 'id' defined for resource: ${resourceName} in service: ${serviceName}`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const resourceDescription = resourceData.description || '';
|
|
443
|
+
|
|
444
|
+
// Determine if it's a View or a Resource
|
|
445
|
+
let resourceType = "Resource"; // Default type
|
|
446
|
+
if (resourceData.config?.views?.select) {
|
|
447
|
+
resourceType = "View";
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
resources.push({
|
|
451
|
+
name: resourceName,
|
|
452
|
+
description: resourceDescription,
|
|
453
|
+
type: resourceType,
|
|
454
|
+
resourceData,
|
|
455
|
+
dereferencedAPI,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Process service index
|
|
460
|
+
const serviceIndexPath = path.join(serviceFolder, 'index.md');
|
|
461
|
+
const serviceIndexContent = await createServiceIndexContent(providerName, serviceName, resources);
|
|
462
|
+
fs.writeFileSync(serviceIndexPath, serviceIndexContent);
|
|
463
|
+
|
|
464
|
+
// Split into columns and process resources one by one
|
|
465
|
+
const halfLength = Math.ceil(resources.length / 2);
|
|
466
|
+
const firstColumn = resources.slice(0, halfLength);
|
|
467
|
+
const secondColumn = resources.slice(halfLength);
|
|
468
|
+
|
|
469
|
+
// Process each resource in first column
|
|
470
|
+
for (const resource of firstColumn) {
|
|
471
|
+
await processResourcev2(providerName, serviceFolder, serviceName, resource, succinct);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Process each resource in second column
|
|
475
|
+
for (const resource of secondColumn) {
|
|
476
|
+
await processResourcev2(providerName, serviceFolder, serviceName, resource, succinct);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
console.log(`Generated documentation (v2) for ${serviceName}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function processResourcev2(providerName, serviceFolder, serviceName, resource, succinct = false) {
|
|
483
|
+
console.log(`Processing resource (v2): ${resource.name}`);
|
|
484
|
+
|
|
485
|
+
const resourceFolder = path.join(serviceFolder, resource.name);
|
|
486
|
+
if (!fs.existsSync(resourceFolder)) {
|
|
487
|
+
fs.mkdirSync(resourceFolder, { recursive: true });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const resourceIndexPath = path.join(resourceFolder, 'index.md');
|
|
491
|
+
const resourceIndexContent = await createResourceIndexContentv2(
|
|
492
|
+
providerName,
|
|
493
|
+
serviceName,
|
|
494
|
+
resource,
|
|
495
|
+
succinct,
|
|
496
|
+
);
|
|
497
|
+
fs.writeFileSync(resourceIndexPath, resourceIndexContent);
|
|
498
|
+
|
|
499
|
+
// After writing the file, force garbage collection if available (optional)
|
|
500
|
+
if (global.gc) {
|
|
501
|
+
global.gc();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
287
505
|
// Function to convert services to markdown links
|
|
288
506
|
function servicesToMarkdown(providerName, servicesList) {
|
|
289
507
|
return servicesList.map(service => `<a href="/services/${service}/">${service}</a><br />`).join('\n');
|
package/src/docgen/helpers.js
CHANGED
|
@@ -90,7 +90,7 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
90
90
|
// Get all SQL verb methods
|
|
91
91
|
const allSqlMethodNames = new Set();
|
|
92
92
|
const sqlVerbTypes = ['select', 'insert', 'update', 'delete', 'replace'];
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
for (const verb of sqlVerbTypes) {
|
|
95
95
|
if (resourceData.sqlVerbs[verb] && resourceData.sqlVerbs[verb].length > 0) {
|
|
96
96
|
for (const method of resourceData.sqlVerbs[verb]) {
|
|
@@ -99,32 +99,32 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
// Process each method that's not in any SQL verb
|
|
104
104
|
for (const [methodName, methodData] of Object.entries(resourceData.methods)) {
|
|
105
105
|
if (!allSqlMethodNames.has(methodName)) {
|
|
106
106
|
const { path, httpVerb, mediaType, openAPIDocKey } = methodData.operation;
|
|
107
107
|
let resolvedPath = path;
|
|
108
108
|
let resolvedVerb = httpVerb;
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
// If operation uses $ref, resolve it
|
|
111
111
|
if (methodData.operation.$ref) {
|
|
112
112
|
const refPath = methodData.operation.$ref;
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
// Extract the path and verb from the $ref
|
|
115
115
|
// The path format is typically '#/paths/~1api~1v2~1accounts~1{name}:undrop/post'
|
|
116
116
|
const pathMatch = refPath.match(/#\/paths\/(.+)\/([^/]+)$/);
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
if (pathMatch && pathMatch.length === 3) {
|
|
119
119
|
// Replace the escaped characters in the path
|
|
120
120
|
let path = pathMatch[1]
|
|
121
121
|
.replace(/~1/g, '/') // Replace ~1 with /
|
|
122
122
|
.replace(/~0/g, '~') // Replace ~0 with ~ if needed
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
// Don't modify path parts with special characters like ':undrop'
|
|
125
125
|
resolvedPath = path;
|
|
126
126
|
resolvedVerb = pathMatch[2];
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
console.log(`Resolved path: ${resolvedPath}, verb: ${resolvedVerb}`);
|
|
129
129
|
} else {
|
|
130
130
|
console.warn(`Could not parse $ref path: ${refPath}`);
|
|
@@ -134,39 +134,41 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// Get response and params using the same function as for SQL verbs
|
|
137
|
-
const { respProps, respDescription, opDescription, requestBody } = getHttpOperationInfo(
|
|
138
|
-
dereferencedAPI,
|
|
139
|
-
resolvedPath,
|
|
140
|
-
resolvedVerb,
|
|
141
|
-
methodData.response.mediaType || '',
|
|
137
|
+
const { respProps, respDescription, opDescription, opSummary, requestBody } = getHttpOperationInfo(
|
|
138
|
+
dereferencedAPI,
|
|
139
|
+
resolvedPath,
|
|
140
|
+
resolvedVerb,
|
|
141
|
+
methodData.response.mediaType || '',
|
|
142
142
|
methodData.response.openAPIDocKey || '200',
|
|
143
143
|
''
|
|
144
144
|
);
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
const { requiredParams, optionalParams } = getHttpOperationParams(
|
|
147
|
-
dereferencedAPI,
|
|
148
|
-
resolvedPath,
|
|
147
|
+
dereferencedAPI,
|
|
148
|
+
resolvedPath,
|
|
149
149
|
resolvedVerb
|
|
150
150
|
);
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
// Initialize the method with the same structure as SQL methods
|
|
153
153
|
methods[methodName] = {
|
|
154
154
|
opDescription,
|
|
155
|
+
opSummary,
|
|
155
156
|
respDescription,
|
|
156
157
|
properties: {},
|
|
157
158
|
requiredParams: requiredParams || {},
|
|
158
159
|
optionalParams: optionalParams || {},
|
|
159
160
|
requestBody: requestBody || {},
|
|
161
|
+
rawRespProps: respProps,
|
|
160
162
|
};
|
|
161
|
-
|
|
163
|
+
|
|
162
164
|
// Format and sort the properties using our helper functions
|
|
163
165
|
const allProperties = formatProperties(respProps);
|
|
164
166
|
sortAndAddProperties(methods[methodName], allProperties);
|
|
165
|
-
|
|
167
|
+
|
|
166
168
|
console.info(`Processed exec method: ${methodName}`);
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
|
|
170
172
|
return methods;
|
|
171
173
|
}
|
|
172
174
|
|
|
@@ -176,26 +178,28 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
176
178
|
|
|
177
179
|
for (const thisMethod of resourceData.sqlVerbs[sqlVerb]) {
|
|
178
180
|
const {path, httpVerb, mediaType, openAPIDocKey, objectKey, methodName} = getHttpOperationForSqlVerb(thisMethod.$ref, resourceData);
|
|
179
|
-
const {respProps, respDescription, opDescription, requestBody} = getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAPIDocKey, objectKey);
|
|
181
|
+
const {respProps, respDescription, opDescription, opSummary, requestBody} = getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAPIDocKey, objectKey);
|
|
180
182
|
const {requiredParams, optionalParams} = getHttpOperationParams(dereferencedAPI, path, httpVerb);
|
|
181
183
|
|
|
182
184
|
// Initialize the method object with description and params
|
|
183
|
-
methods[methodName] = {
|
|
185
|
+
methods[methodName] = {
|
|
184
186
|
opDescription,
|
|
187
|
+
opSummary,
|
|
185
188
|
respDescription,
|
|
186
189
|
properties: {},
|
|
187
190
|
requiredParams: requiredParams || {},
|
|
188
191
|
optionalParams: optionalParams || {},
|
|
189
192
|
requestBody: requestBody || {},
|
|
193
|
+
rawRespProps: respProps,
|
|
190
194
|
};
|
|
191
|
-
|
|
195
|
+
|
|
192
196
|
// Format and sort the properties using our helper functions
|
|
193
197
|
const allProperties = formatProperties(respProps);
|
|
194
198
|
sortAndAddProperties(methods[methodName], allProperties);
|
|
195
|
-
|
|
199
|
+
|
|
196
200
|
console.info(`Processed method: ${methodName}`);
|
|
197
201
|
}
|
|
198
|
-
|
|
202
|
+
|
|
199
203
|
return methods;
|
|
200
204
|
}
|
|
201
205
|
|
|
@@ -381,8 +385,9 @@ function getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAP
|
|
|
381
385
|
throw new Error(`HTTP verb '${httpVerb}' not found for path '${path}'`);
|
|
382
386
|
}
|
|
383
387
|
|
|
384
|
-
// Get operation description
|
|
388
|
+
// Get operation description and summary
|
|
385
389
|
const opDescription = (dereferencedAPI.paths[path][httpVerb].description || '');
|
|
390
|
+
const opSummary = (dereferencedAPI.paths[path][httpVerb].summary || '');
|
|
386
391
|
|
|
387
392
|
// Extract request body if it exists
|
|
388
393
|
let requestBody = {};
|
|
@@ -472,19 +477,21 @@ function getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAP
|
|
|
472
477
|
respProps: {},
|
|
473
478
|
respDescription: '',
|
|
474
479
|
opDescription,
|
|
480
|
+
opSummary,
|
|
475
481
|
requestBody
|
|
476
482
|
};
|
|
477
483
|
}
|
|
478
|
-
|
|
484
|
+
|
|
479
485
|
// Check if there's a content section with the mediaType
|
|
480
486
|
const responseObj = dereferencedAPI.paths[path][httpVerb].responses[openAPIDocKey];
|
|
481
|
-
|
|
487
|
+
|
|
482
488
|
// If no content or no mediaType in the response, return empty properties
|
|
483
489
|
if (!responseObj.content || !mediaType || !responseObj.content[mediaType] || !responseObj.content[mediaType].schema) {
|
|
484
490
|
return {
|
|
485
491
|
respProps: {},
|
|
486
492
|
respDescription: responseObj.description || '',
|
|
487
493
|
opDescription,
|
|
494
|
+
opSummary,
|
|
488
495
|
requestBody
|
|
489
496
|
};
|
|
490
497
|
}
|
|
@@ -497,6 +504,7 @@ function getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAP
|
|
|
497
504
|
respProps,
|
|
498
505
|
respDescription: responseObj.description ? responseObj.description : respDescription,
|
|
499
506
|
opDescription,
|
|
507
|
+
opSummary,
|
|
500
508
|
requestBody
|
|
501
509
|
};
|
|
502
510
|
}
|
|
@@ -569,6 +577,81 @@ function getHttpRespBody(schema, objectKey) {
|
|
|
569
577
|
}
|
|
570
578
|
}
|
|
571
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Recursively generates a nested JSON schema tree from response properties,
|
|
582
|
+
* suitable for use with the SchemaTable React component.
|
|
583
|
+
* Each node has: { name, type, description, children? }
|
|
584
|
+
* @param {Object} respProps - The raw response properties from the dereferenced API
|
|
585
|
+
* @param {number} depth - Current recursion depth
|
|
586
|
+
* @param {number} maxDepth - Maximum recursion depth to prevent infinite loops
|
|
587
|
+
* @returns {Array} Array of schema field objects
|
|
588
|
+
*/
|
|
589
|
+
export function generateSchemaJsonFromProps(respProps, depth = 0, maxDepth = 4) {
|
|
590
|
+
if (depth >= maxDepth || !respProps || typeof respProps !== 'object') return [];
|
|
591
|
+
|
|
592
|
+
return Object.entries(respProps).map(([propName, prop]) => {
|
|
593
|
+
if (!prop) return null;
|
|
594
|
+
|
|
595
|
+
let propType = prop.type || 'object';
|
|
596
|
+
let propDesc = sanitizeHtml(prop.description || '');
|
|
597
|
+
|
|
598
|
+
// Add format info to type string if available
|
|
599
|
+
if (prop.format) {
|
|
600
|
+
propType += ` (${prop.format})`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
let children = [];
|
|
604
|
+
|
|
605
|
+
// Handle arrays - get children from items
|
|
606
|
+
if (prop.type === 'array' && prop.items) {
|
|
607
|
+
if (prop.items.properties) {
|
|
608
|
+
children = generateSchemaJsonFromProps(prop.items.properties, depth + 1, maxDepth);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Handle objects - get children from properties
|
|
613
|
+
if (prop.type === 'object' && prop.properties) {
|
|
614
|
+
children = generateSchemaJsonFromProps(prop.properties, depth + 1, maxDepth);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
name: propName,
|
|
619
|
+
type: propType,
|
|
620
|
+
description: propDesc,
|
|
621
|
+
...(children.length > 0 && { children })
|
|
622
|
+
};
|
|
623
|
+
}).filter(Boolean);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Sorts an array of schema field objects using the standard field priority:
|
|
628
|
+
* 1. Exact 'id' and 'name' fields
|
|
629
|
+
* 2. Fields ending with '_id'
|
|
630
|
+
* 3. Fields ending with '_name'
|
|
631
|
+
* 4. All other fields (alphabetical)
|
|
632
|
+
* @param {Array} fields - Array of schema field objects with 'name' property
|
|
633
|
+
* @returns {Array} Sorted array of schema field objects
|
|
634
|
+
*/
|
|
635
|
+
export function sortSchemaFields(fields) {
|
|
636
|
+
if (!fields || fields.length === 0) return [];
|
|
637
|
+
|
|
638
|
+
const exactIdName = fields.filter(f => f.name === 'id' || f.name === 'name');
|
|
639
|
+
const idSuffix = fields.filter(f => f.name !== 'id' && f.name.endsWith('_id'));
|
|
640
|
+
const nameSuffix = fields.filter(f => f.name !== 'name' && f.name.endsWith('_name'));
|
|
641
|
+
const others = fields.filter(f =>
|
|
642
|
+
!exactIdName.includes(f) &&
|
|
643
|
+
!idSuffix.includes(f) &&
|
|
644
|
+
!nameSuffix.includes(f)
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
return [
|
|
648
|
+
...exactIdName.sort((a, b) => a.name.localeCompare(b.name)),
|
|
649
|
+
...idSuffix.sort((a, b) => a.name.localeCompare(b.name)),
|
|
650
|
+
...nameSuffix.sort((a, b) => a.name.localeCompare(b.name)),
|
|
651
|
+
...others.sort((a, b) => a.name.localeCompare(b.name)),
|
|
652
|
+
];
|
|
653
|
+
}
|
|
654
|
+
|
|
572
655
|
function getHttpOperationParams(dereferencedAPI, path, httpVerb) {
|
|
573
656
|
const requiredParams = {};
|
|
574
657
|
const optionalParams = {};
|
package/src/docgen/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/docgen/index.js
|
|
2
2
|
|
|
3
3
|
// Export all documentation generation functions
|
|
4
|
-
export { generateDocs } from './generator.js';
|
|
5
|
-
export { createResourceIndexContent } from './resource-content.js';
|
|
4
|
+
export { generateDocs, generateDocsv2 } from './generator.js';
|
|
5
|
+
export { createResourceIndexContent, createResourceIndexContentv2 } from './resource-content.js';
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
sanitizeHtml,
|
|
5
5
|
} from '../../helpers.js';
|
|
6
6
|
|
|
7
|
-
export function createDeleteExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI) {
|
|
7
|
+
export function createDeleteExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct = false) {
|
|
8
8
|
const deleteMethods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'delete');
|
|
9
9
|
|
|
10
10
|
// if there are no delete methods, return empty content
|
|
@@ -30,7 +30,7 @@ export function createDeleteExamples(providerName, serviceName, resourceName, re
|
|
|
30
30
|
content += '<TabItem value="' + methodName + '">\n\n';
|
|
31
31
|
|
|
32
32
|
// Add method description
|
|
33
|
-
const opDescription = methodDetails.opDescription || 'No description available.';
|
|
33
|
+
const opDescription = (succinct && methodDetails.opSummary) ? methodDetails.opSummary : (methodDetails.opDescription || 'No description available.');
|
|
34
34
|
content += sanitizeHtml(opDescription);
|
|
35
35
|
|
|
36
36
|
// Create SQL example
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
sanitizeHtml
|
|
5
5
|
} from '../../helpers.js';
|
|
6
6
|
|
|
7
|
-
export function createExecExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI) {
|
|
7
|
+
export function createExecExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct = false) {
|
|
8
8
|
const execMethods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'exec');
|
|
9
9
|
|
|
10
10
|
// if there are no exec methods, return empty content
|
|
@@ -30,7 +30,7 @@ export function createExecExamples(providerName, serviceName, resourceName, reso
|
|
|
30
30
|
content += '<TabItem value="' + methodName + '">\n\n';
|
|
31
31
|
|
|
32
32
|
// Add method description
|
|
33
|
-
const opDescription = methodDetails.opDescription || methodDetails.respDescription || 'No description available.';
|
|
33
|
+
const opDescription = (succinct && methodDetails.opSummary) ? methodDetails.opSummary : (methodDetails.opDescription || methodDetails.respDescription || 'No description available.');
|
|
34
34
|
content += sanitizeHtml(opDescription);
|
|
35
35
|
|
|
36
36
|
// Create SQL example
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
sanitizeHtml
|
|
5
5
|
} from '../../helpers.js';
|
|
6
6
|
|
|
7
|
-
export function createInsertExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI) {
|
|
7
|
+
export function createInsertExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct = false) {
|
|
8
8
|
const insertMethods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'insert');
|
|
9
9
|
|
|
10
10
|
// if there are no insert methods, return empty content
|
|
@@ -34,9 +34,8 @@ export function createInsertExamples(providerName, serviceName, resourceName, re
|
|
|
34
34
|
content += '<TabItem value="' + methodName + '">\n\n';
|
|
35
35
|
|
|
36
36
|
// Add method description
|
|
37
|
-
const opDescription = methodDetails.opDescription || 'No description available.';
|
|
37
|
+
const opDescription = (succinct && methodDetails.opSummary) ? methodDetails.opSummary : (methodDetails.opDescription || 'No description available.');
|
|
38
38
|
content += sanitizeHtml(opDescription);
|
|
39
|
-
// content += methodDetails.opDescription || 'No description available.';
|
|
40
39
|
|
|
41
40
|
// Create SQL example
|
|
42
41
|
content += '\n\n```sql\nINSERT INTO ' + providerName + '.' + serviceName + '.' + resourceName + ' (\n';
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
sanitizeHtml
|
|
5
5
|
} from '../../helpers.js';
|
|
6
6
|
|
|
7
|
-
export function createSelectExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI) {
|
|
7
|
+
export function createSelectExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct = false) {
|
|
8
8
|
const selectMethods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'select');
|
|
9
9
|
|
|
10
10
|
// if there are no select methods, return empty content
|
|
@@ -30,7 +30,7 @@ export function createSelectExamples(providerName, serviceName, resourceName, re
|
|
|
30
30
|
content += '<TabItem value="' + methodName + '">\n\n';
|
|
31
31
|
// content += methodDetails.opDescription || 'No description available.';
|
|
32
32
|
// Add method description
|
|
33
|
-
const opDescription = methodDetails.opDescription || methodDetails.respDescription || 'No description available.';
|
|
33
|
+
const opDescription = (succinct && methodDetails.opSummary) ? methodDetails.opSummary : (methodDetails.opDescription || methodDetails.respDescription || 'No description available.');
|
|
34
34
|
content += sanitizeHtml(opDescription);
|
|
35
35
|
|
|
36
36
|
// Create SQL example
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
sanitizeHtml
|
|
5
5
|
} from '../../helpers.js';
|
|
6
6
|
|
|
7
|
-
export function createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, isReplace = false) {
|
|
7
|
+
export function createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, isReplace = false, succinct = false) {
|
|
8
8
|
const updateMethods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, isReplace ? 'replace' : 'update');
|
|
9
9
|
|
|
10
10
|
// if there are no update methods, return empty content
|
|
@@ -34,10 +34,8 @@ export function createUpdateExamples(providerName, serviceName, resourceName, re
|
|
|
34
34
|
Object.entries(updateMethods).forEach(([methodName, methodDetails]) => {
|
|
35
35
|
content += '<TabItem value="' + methodName + '">\n\n';
|
|
36
36
|
|
|
37
|
-
// // Add method description
|
|
38
|
-
// content += methodDetails.opDescription || methodDetails.respDescription || 'No description available.';
|
|
39
37
|
// Add method description
|
|
40
|
-
const opDescription = methodDetails.opDescription || 'No description available.';
|
|
38
|
+
const opDescription = (succinct && methodDetails.opSummary) ? methodDetails.opSummary : (methodDetails.opDescription || 'No description available.');
|
|
41
39
|
content += sanitizeHtml(opDescription);
|
|
42
40
|
|
|
43
41
|
// Create SQL example
|
|
@@ -6,26 +6,26 @@ import { createUpdateExamples } from './examples/update-example.js';
|
|
|
6
6
|
import { createDeleteExamples } from './examples/delete-example.js';
|
|
7
7
|
import { createExecExamples } from './examples/exec-example.js';
|
|
8
8
|
|
|
9
|
-
export function createExamplesSection(providerName, serviceName, resourceName, resourceData, dereferencedAPI) {
|
|
9
|
+
export function createExamplesSection(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct = false) {
|
|
10
10
|
let content = '';
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
// Add SELECT examples
|
|
13
|
-
content += createSelectExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI);
|
|
13
|
+
content += createSelectExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct);
|
|
14
14
|
|
|
15
15
|
// Add INSERT examples
|
|
16
|
-
content += createInsertExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI);
|
|
17
|
-
|
|
16
|
+
content += createInsertExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct);
|
|
17
|
+
|
|
18
18
|
// Add UPDATE examples
|
|
19
|
-
content += createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, false);
|
|
20
|
-
|
|
19
|
+
content += createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, false, succinct);
|
|
20
|
+
|
|
21
21
|
// Add REPLACE examples
|
|
22
|
-
content += createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, true);
|
|
22
|
+
content += createUpdateExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, true, succinct);
|
|
23
23
|
|
|
24
24
|
// Add DELETE examples
|
|
25
|
-
content += createDeleteExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI);
|
|
25
|
+
content += createDeleteExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct);
|
|
26
26
|
|
|
27
27
|
// Add EXEC examples
|
|
28
|
-
content += createExecExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI);
|
|
28
|
+
content += createExecExamples(providerName, serviceName, resourceName, resourceData, dereferencedAPI, succinct);
|
|
29
29
|
|
|
30
30
|
return content;
|
|
31
31
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/docgen/resource/fields.js
|
|
2
|
-
import {
|
|
3
|
-
getSqlMethodsWithOrderedFields,
|
|
2
|
+
import {
|
|
3
|
+
getSqlMethodsWithOrderedFields,
|
|
4
4
|
sanitizeHtml,
|
|
5
|
+
generateSchemaJsonFromProps,
|
|
6
|
+
sortSchemaFields,
|
|
5
7
|
} from '../helpers.js';
|
|
6
8
|
import { docView } from './view.js';
|
|
7
9
|
|
|
@@ -100,3 +102,62 @@ export function createFieldsSection(resourceType, resourceData, dereferencedAPI)
|
|
|
100
102
|
return content;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
export function createFieldsSectionv2(resourceType, resourceData, dereferencedAPI) {
|
|
106
|
+
let content = '## Fields\n\n';
|
|
107
|
+
|
|
108
|
+
if (resourceType === 'Resource') {
|
|
109
|
+
|
|
110
|
+
content += 'The following fields are returned by `SELECT` queries:\n\n';
|
|
111
|
+
|
|
112
|
+
// Use the reusable function to get methods with ordered fields
|
|
113
|
+
const methods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'select');
|
|
114
|
+
|
|
115
|
+
if (Object.keys(methods).length > 0) {
|
|
116
|
+
const methodNames = Object.keys(methods);
|
|
117
|
+
|
|
118
|
+
const tabValues = methodNames.map(methodName => {
|
|
119
|
+
return `{ label: '${methodName}', value: '${methodName}' }`;
|
|
120
|
+
}).join(',\n ');
|
|
121
|
+
|
|
122
|
+
content += `<Tabs
|
|
123
|
+
defaultValue="${methodNames[0]}"
|
|
124
|
+
values={[
|
|
125
|
+
${tabValues}
|
|
126
|
+
]}
|
|
127
|
+
>\n`;
|
|
128
|
+
|
|
129
|
+
for (const methodName of methodNames) {
|
|
130
|
+
const methodData = methods[methodName];
|
|
131
|
+
|
|
132
|
+
content += `<TabItem value="${methodName}">\n\n`;
|
|
133
|
+
|
|
134
|
+
// Add the method description if available and not in the meaningless list
|
|
135
|
+
if (methodData.respDescription &&
|
|
136
|
+
!meaninglessDescriptions.includes(methodData.respDescription.trim().toLowerCase()) &&
|
|
137
|
+
methodData.respDescription.trim().length > 0) {
|
|
138
|
+
content += `${sanitizeHtml(methodData.respDescription)}\n\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build nested schema JSON from raw response props and sort fields
|
|
142
|
+
const schemaFields = generateSchemaJsonFromProps(methodData.rawRespProps);
|
|
143
|
+
const sortedFields = sortSchemaFields(schemaFields);
|
|
144
|
+
|
|
145
|
+
content += `<SchemaTable fields={${JSON.stringify(sortedFields, null, 2)}} />\n`;
|
|
146
|
+
|
|
147
|
+
content += `</TabItem>\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
content += `</Tabs>\n`;
|
|
151
|
+
} else {
|
|
152
|
+
content += `${mdCodeAnchor}SELECT${mdCodeAnchor} not supported for this resource, use ${mdCodeAnchor}SHOW METHODS${mdCodeAnchor} to view available operations for the resource.\n\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
} else {
|
|
156
|
+
// its a view
|
|
157
|
+
console.log(`processing view : ${resourceData.name}...`)
|
|
158
|
+
content += docView(resourceData);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return content;
|
|
162
|
+
}
|
|
163
|
+
|
|
@@ -22,7 +22,7 @@ const getRequiredBodyParams = (methodDetails, accessType) => {
|
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export function createMethodsSection(resourceData, dereferencedAPI) {
|
|
25
|
+
export function createMethodsSection(resourceData, dereferencedAPI, succinct = false) {
|
|
26
26
|
|
|
27
27
|
let content = `\n## Methods\n\n`;
|
|
28
28
|
|
|
@@ -80,7 +80,7 @@ export function createMethodsSection(resourceData, dereferencedAPI) {
|
|
|
80
80
|
<td><CopyableCode code="${accessType}" /></td>
|
|
81
81
|
<td>${requiredParamsStr}</td>
|
|
82
82
|
<td>${optionalParamsStr}</td>
|
|
83
|
-
<td>${sanitizeHtml(methodDetails.opDescription)}</td>
|
|
83
|
+
<td>${sanitizeHtml(succinct && methodDetails.opSummary ? methodDetails.opSummary : methodDetails.opDescription)}</td>
|
|
84
84
|
</tr>`;
|
|
85
85
|
}
|
|
86
86
|
};
|
|
@@ -41,3 +41,43 @@ content += `
|
|
|
41
41
|
`;
|
|
42
42
|
return content;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export function createOverviewSectionv2(resourceName, resourceType, resourceDescription, providerName, serviceName) {
|
|
46
|
+
|
|
47
|
+
let content = `---
|
|
48
|
+
title: ${resourceName}
|
|
49
|
+
hide_title: false
|
|
50
|
+
hide_table_of_contents: false
|
|
51
|
+
keywords:
|
|
52
|
+
- ${resourceName}
|
|
53
|
+
- ${serviceName}
|
|
54
|
+
- ${providerName}
|
|
55
|
+
- infrastructure-as-code
|
|
56
|
+
- configuration-as-data
|
|
57
|
+
- cloud inventory
|
|
58
|
+
description: Query, deploy and manage ${providerName} resources using SQL
|
|
59
|
+
custom_edit_url: null
|
|
60
|
+
image: /img/stackql-${providerName}-provider-featured-image.png
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
import CopyableCode from '@site/src/components/CopyableCode/CopyableCode';
|
|
64
|
+
import Tabs from '@theme/Tabs';
|
|
65
|
+
import TabItem from '@theme/TabItem';
|
|
66
|
+
import SchemaTable from '@site/src/components/SchemaTable/SchemaTable';
|
|
67
|
+
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
content += resourceDescription ? resourceDescription : `Creates, updates, deletes, gets or lists ${getIndefiniteArticle(resourceName)} <code>${resourceName}</code> resource.`;
|
|
71
|
+
|
|
72
|
+
content += `
|
|
73
|
+
|
|
74
|
+
## Overview
|
|
75
|
+
<table><tbody>
|
|
76
|
+
<tr><td><b>Name</b></td><td><code>${resourceName}</code></td></tr>
|
|
77
|
+
<tr><td><b>Type</b></td><td>${resourceType}</td></tr>
|
|
78
|
+
<tr><td><b>Id</b></td><td><CopyableCode code="${providerName}.${serviceName}.${resourceName}" /></td></tr>
|
|
79
|
+
</tbody></table>
|
|
80
|
+
|
|
81
|
+
`;
|
|
82
|
+
return content;
|
|
83
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// src/docgen/resource-content.js
|
|
2
2
|
|
|
3
|
-
import { createOverviewSection } from './resource/overview.js';
|
|
4
|
-
import { createFieldsSection } from './resource/fields.js';
|
|
3
|
+
import { createOverviewSection, createOverviewSectionv2 } from './resource/overview.js';
|
|
4
|
+
import { createFieldsSection, createFieldsSectionv2 } from './resource/fields.js';
|
|
5
5
|
import { createMethodsSection } from './resource/methods.js';
|
|
6
6
|
import { createParamsSection } from './resource/parameters.js';
|
|
7
7
|
import { createExamplesSection } from './resource/examples.js';
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
export async function createResourceIndexContent(
|
|
10
|
-
providerName,
|
|
11
|
-
serviceName,
|
|
10
|
+
providerName,
|
|
11
|
+
serviceName,
|
|
12
12
|
resource,
|
|
13
13
|
) {
|
|
14
14
|
// Generate each section of the documentation
|
|
@@ -21,3 +21,20 @@ export async function createResourceIndexContent(
|
|
|
21
21
|
// Combine all sections into the final content
|
|
22
22
|
return `${overviewContent}${fieldsContent}${methodsContent}${paramsContent}${examplesContent}`;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export async function createResourceIndexContentv2(
|
|
26
|
+
providerName,
|
|
27
|
+
serviceName,
|
|
28
|
+
resource,
|
|
29
|
+
succinct = false,
|
|
30
|
+
) {
|
|
31
|
+
// Generate each section of the documentation (v2 uses SchemaTable for fields)
|
|
32
|
+
const overviewContent = createOverviewSectionv2(resource.name, resource.type, resource.description, providerName, serviceName);
|
|
33
|
+
const fieldsContent = createFieldsSectionv2(resource.type, resource.resourceData, resource.dereferencedAPI);
|
|
34
|
+
const methodsContent = resource.type === 'Resource' ? createMethodsSection(resource.resourceData, resource.dereferencedAPI, succinct) : '';
|
|
35
|
+
const paramsContent = resource.type === 'Resource' ? createParamsSection(resource.resourceData, resource.dereferencedAPI) : '';
|
|
36
|
+
const examplesContent = resource.type === 'Resource' ? createExamplesSection(providerName, serviceName, resource.name, resource.resourceData, resource.dereferencedAPI, succinct) : '';
|
|
37
|
+
|
|
38
|
+
// Combine all sections into the final content
|
|
39
|
+
return `${overviewContent}${fieldsContent}${methodsContent}${paramsContent}${examplesContent}`;
|
|
40
|
+
}
|