@stackql/provider-utils 0.5.4 → 0.5.6
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
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,222 @@ 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
|
+
} = options;
|
|
295
|
+
|
|
296
|
+
console.log(`documenting ${providerName} (v2)...`);
|
|
297
|
+
|
|
298
|
+
const docsDir = path.join(outputDir, `docs`);
|
|
299
|
+
const servicesDir = path.join(docsDir, `services`);
|
|
300
|
+
|
|
301
|
+
// Remove directory if it exists, then create it fresh
|
|
302
|
+
fs.existsSync(docsDir) && fs.rmSync(docsDir, { recursive: true, force: true });
|
|
303
|
+
fs.mkdirSync(servicesDir, { recursive: true });
|
|
304
|
+
|
|
305
|
+
// Check for provider data files
|
|
306
|
+
console.log(providerDataDir);
|
|
307
|
+
try {
|
|
308
|
+
const files = fs.readdirSync(providerDataDir);
|
|
309
|
+
console.log('Files in providerDataDir:', files);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error('Error reading providerDataDir:', err.message);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const headerContent1Path = path.join(providerDataDir, 'headerContent1.txt');
|
|
315
|
+
const headerContent2Path = path.join(providerDataDir, 'headerContent2.txt');
|
|
316
|
+
|
|
317
|
+
if (!fs.existsSync(headerContent1Path) || !fs.existsSync(headerContent2Path)) {
|
|
318
|
+
throw new Error(`Missing headerContent1.txt or headerContent2.txt in ${providerDataDir}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const headerContent1 = fs.readFileSync(headerContent1Path, 'utf8');
|
|
322
|
+
const headerContent2 = fs.readFileSync(headerContent2Path, 'utf8');
|
|
323
|
+
|
|
324
|
+
// Initialize counters
|
|
325
|
+
let servicesForIndex = [];
|
|
326
|
+
let totalServicesCount = 0;
|
|
327
|
+
let totalResourcesCount = 0;
|
|
328
|
+
|
|
329
|
+
// Process services
|
|
330
|
+
const serviceDir = path.join(providerDir, 'services');
|
|
331
|
+
console.log(`Processing services in ${serviceDir}...`);
|
|
332
|
+
const serviceFiles = fs.readdirSync(serviceDir).filter(file => path.extname(file) === '.yaml');
|
|
333
|
+
|
|
334
|
+
for (const file of serviceFiles) {
|
|
335
|
+
const serviceName = path.basename(file, '.yaml').replace(/-/g, '_');
|
|
336
|
+
console.log(`Processing service: ${serviceName}`);
|
|
337
|
+
servicesForIndex.push(serviceName);
|
|
338
|
+
const filePath = path.join(serviceDir, file);
|
|
339
|
+
totalServicesCount++;
|
|
340
|
+
const serviceFolder = `${servicesDir}/${serviceName}`;
|
|
341
|
+
await createDocsForServicev2(filePath, providerName, serviceName, serviceFolder, dereferenced);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(`Processed ${totalServicesCount} services`);
|
|
345
|
+
|
|
346
|
+
// Count total resources
|
|
347
|
+
totalResourcesCount = fs.readdirSync(`${servicesDir}`, { withFileTypes: true })
|
|
348
|
+
.filter(dirent => dirent.isDirectory())
|
|
349
|
+
.map(dirent => fs.readdirSync(`${servicesDir}/${dirent.name}`).length)
|
|
350
|
+
.reduce((a, b) => a + b, 0);
|
|
351
|
+
|
|
352
|
+
console.log(`Processed ${totalResourcesCount} resources`);
|
|
353
|
+
|
|
354
|
+
// Create provider index
|
|
355
|
+
servicesForIndex = [...new Set(servicesForIndex)];
|
|
356
|
+
servicesForIndex.sort();
|
|
357
|
+
|
|
358
|
+
const half = Math.ceil(servicesForIndex.length / 2);
|
|
359
|
+
const firstColumnServices = servicesForIndex.slice(0, half);
|
|
360
|
+
const secondColumnServices = servicesForIndex.slice(half);
|
|
361
|
+
|
|
362
|
+
const indexContent = `${headerContent1}
|
|
363
|
+
|
|
364
|
+
:::info[Provider Summary]
|
|
365
|
+
|
|
366
|
+
total services: __${totalServicesCount}__
|
|
367
|
+
total resources: __${totalResourcesCount}__
|
|
368
|
+
|
|
369
|
+
:::
|
|
370
|
+
|
|
371
|
+
${headerContent2}
|
|
372
|
+
|
|
373
|
+
## Services
|
|
374
|
+
<div class="row">
|
|
375
|
+
<div class="providerDocColumn">
|
|
376
|
+
${servicesToMarkdown(providerName, firstColumnServices)}
|
|
377
|
+
</div>
|
|
378
|
+
<div class="providerDocColumn">
|
|
379
|
+
${servicesToMarkdown(providerName, secondColumnServices)}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
// Write index
|
|
385
|
+
const indexPath = path.join(docsDir, 'index.md');
|
|
386
|
+
fs.writeFileSync(indexPath, indexContent);
|
|
387
|
+
console.log(`Index file created at ${indexPath}`);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
totalServices: totalServicesCount,
|
|
391
|
+
totalResources: totalResourcesCount,
|
|
392
|
+
outputPath: docsDir
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// v2 service processing - uses SchemaTable for collapsible nested fields
|
|
397
|
+
async function createDocsForServicev2(yamlFilePath, providerName, serviceName, serviceFolder, dereferenced = false) {
|
|
398
|
+
|
|
399
|
+
const data = yaml.load(fs.readFileSync(yamlFilePath, 'utf8'));
|
|
400
|
+
|
|
401
|
+
// Create a new SwaggerParser instance
|
|
402
|
+
let parser = new SwaggerParser();
|
|
403
|
+
const api = await parser.parse(yamlFilePath);
|
|
404
|
+
const ignorePaths = ["$.components.x-stackQL-resources"];
|
|
405
|
+
let dereferencedAPI;
|
|
406
|
+
|
|
407
|
+
if (dereferenced) {
|
|
408
|
+
// If API is already dereferenced, just use it as is
|
|
409
|
+
dereferencedAPI = api;
|
|
410
|
+
} else {
|
|
411
|
+
try {
|
|
412
|
+
// Only dereference and flatten if needed
|
|
413
|
+
dereferencedAPI = await SwaggerParser.dereference(api);
|
|
414
|
+
dereferencedAPI = await deno_openapi_dereferencer.flattenAllOf(dereferencedAPI);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error("error in dereferencing or flattening:", error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Create service directory
|
|
421
|
+
if (!fs.existsSync(serviceFolder)) {
|
|
422
|
+
fs.mkdirSync(serviceFolder, { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const resourcesObj = data.components['x-stackQL-resources'];
|
|
426
|
+
|
|
427
|
+
if (!resourcesObj) {
|
|
428
|
+
console.warn(`No resources found in ${yamlFilePath}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const resources = [];
|
|
433
|
+
for (let resourceName in resourcesObj) {
|
|
434
|
+
|
|
435
|
+
let resourceData = resourcesObj[resourceName];
|
|
436
|
+
if (!resourceData.id) {
|
|
437
|
+
console.warn(`No 'id' defined for resource: ${resourceName} in service: ${serviceName}`);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const resourceDescription = resourceData.description || '';
|
|
442
|
+
|
|
443
|
+
// Determine if it's a View or a Resource
|
|
444
|
+
let resourceType = "Resource"; // Default type
|
|
445
|
+
if (resourceData.config?.views?.select) {
|
|
446
|
+
resourceType = "View";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
resources.push({
|
|
450
|
+
name: resourceName,
|
|
451
|
+
description: resourceDescription,
|
|
452
|
+
type: resourceType,
|
|
453
|
+
resourceData,
|
|
454
|
+
dereferencedAPI,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Process service index
|
|
459
|
+
const serviceIndexPath = path.join(serviceFolder, 'index.md');
|
|
460
|
+
const serviceIndexContent = await createServiceIndexContent(providerName, serviceName, resources);
|
|
461
|
+
fs.writeFileSync(serviceIndexPath, serviceIndexContent);
|
|
462
|
+
|
|
463
|
+
// Split into columns and process resources one by one
|
|
464
|
+
const halfLength = Math.ceil(resources.length / 2);
|
|
465
|
+
const firstColumn = resources.slice(0, halfLength);
|
|
466
|
+
const secondColumn = resources.slice(halfLength);
|
|
467
|
+
|
|
468
|
+
// Process each resource in first column
|
|
469
|
+
for (const resource of firstColumn) {
|
|
470
|
+
await processResourcev2(providerName, serviceFolder, serviceName, resource);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Process each resource in second column
|
|
474
|
+
for (const resource of secondColumn) {
|
|
475
|
+
await processResourcev2(providerName, serviceFolder, serviceName, resource);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
console.log(`Generated documentation (v2) for ${serviceName}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function processResourcev2(providerName, serviceFolder, serviceName, resource) {
|
|
482
|
+
console.log(`Processing resource (v2): ${resource.name}`);
|
|
483
|
+
|
|
484
|
+
const resourceFolder = path.join(serviceFolder, resource.name);
|
|
485
|
+
if (!fs.existsSync(resourceFolder)) {
|
|
486
|
+
fs.mkdirSync(resourceFolder, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const resourceIndexPath = path.join(resourceFolder, 'index.md');
|
|
490
|
+
const resourceIndexContent = await createResourceIndexContentv2(
|
|
491
|
+
providerName,
|
|
492
|
+
serviceName,
|
|
493
|
+
resource,
|
|
494
|
+
);
|
|
495
|
+
fs.writeFileSync(resourceIndexPath, resourceIndexContent);
|
|
496
|
+
|
|
497
|
+
// After writing the file, force garbage collection if available (optional)
|
|
498
|
+
if (global.gc) {
|
|
499
|
+
global.gc();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
287
503
|
// Function to convert services to markdown links
|
|
288
504
|
function servicesToMarkdown(providerName, servicesList) {
|
|
289
505
|
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}`);
|
|
@@ -135,20 +135,20 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
135
135
|
|
|
136
136
|
// Get response and params using the same function as for SQL verbs
|
|
137
137
|
const { respProps, respDescription, opDescription, requestBody } = getHttpOperationInfo(
|
|
138
|
-
dereferencedAPI,
|
|
139
|
-
resolvedPath,
|
|
140
|
-
resolvedVerb,
|
|
141
|
-
methodData.response.mediaType || '',
|
|
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,
|
|
@@ -157,16 +157,17 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
157
157
|
requiredParams: requiredParams || {},
|
|
158
158
|
optionalParams: optionalParams || {},
|
|
159
159
|
requestBody: requestBody || {},
|
|
160
|
+
rawRespProps: respProps,
|
|
160
161
|
};
|
|
161
|
-
|
|
162
|
+
|
|
162
163
|
// Format and sort the properties using our helper functions
|
|
163
164
|
const allProperties = formatProperties(respProps);
|
|
164
165
|
sortAndAddProperties(methods[methodName], allProperties);
|
|
165
|
-
|
|
166
|
+
|
|
166
167
|
console.info(`Processed exec method: ${methodName}`);
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
|
-
|
|
170
|
+
|
|
170
171
|
return methods;
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -180,22 +181,23 @@ export function getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, sq
|
|
|
180
181
|
const {requiredParams, optionalParams} = getHttpOperationParams(dereferencedAPI, path, httpVerb);
|
|
181
182
|
|
|
182
183
|
// Initialize the method object with description and params
|
|
183
|
-
methods[methodName] = {
|
|
184
|
+
methods[methodName] = {
|
|
184
185
|
opDescription,
|
|
185
186
|
respDescription,
|
|
186
187
|
properties: {},
|
|
187
188
|
requiredParams: requiredParams || {},
|
|
188
189
|
optionalParams: optionalParams || {},
|
|
189
190
|
requestBody: requestBody || {},
|
|
191
|
+
rawRespProps: respProps,
|
|
190
192
|
};
|
|
191
|
-
|
|
193
|
+
|
|
192
194
|
// Format and sort the properties using our helper functions
|
|
193
195
|
const allProperties = formatProperties(respProps);
|
|
194
196
|
sortAndAddProperties(methods[methodName], allProperties);
|
|
195
|
-
|
|
197
|
+
|
|
196
198
|
console.info(`Processed method: ${methodName}`);
|
|
197
199
|
}
|
|
198
|
-
|
|
200
|
+
|
|
199
201
|
return methods;
|
|
200
202
|
}
|
|
201
203
|
|
|
@@ -569,6 +571,81 @@ function getHttpRespBody(schema, objectKey) {
|
|
|
569
571
|
}
|
|
570
572
|
}
|
|
571
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Recursively generates a nested JSON schema tree from response properties,
|
|
576
|
+
* suitable for use with the SchemaTable React component.
|
|
577
|
+
* Each node has: { name, type, description, children? }
|
|
578
|
+
* @param {Object} respProps - The raw response properties from the dereferenced API
|
|
579
|
+
* @param {number} depth - Current recursion depth
|
|
580
|
+
* @param {number} maxDepth - Maximum recursion depth to prevent infinite loops
|
|
581
|
+
* @returns {Array} Array of schema field objects
|
|
582
|
+
*/
|
|
583
|
+
export function generateSchemaJsonFromProps(respProps, depth = 0, maxDepth = 4) {
|
|
584
|
+
if (depth >= maxDepth || !respProps || typeof respProps !== 'object') return [];
|
|
585
|
+
|
|
586
|
+
return Object.entries(respProps).map(([propName, prop]) => {
|
|
587
|
+
if (!prop) return null;
|
|
588
|
+
|
|
589
|
+
let propType = prop.type || 'object';
|
|
590
|
+
let propDesc = sanitizeHtml(prop.description || '');
|
|
591
|
+
|
|
592
|
+
// Add format info to type string if available
|
|
593
|
+
if (prop.format) {
|
|
594
|
+
propType += ` (${prop.format})`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let children = [];
|
|
598
|
+
|
|
599
|
+
// Handle arrays - get children from items
|
|
600
|
+
if (prop.type === 'array' && prop.items) {
|
|
601
|
+
if (prop.items.properties) {
|
|
602
|
+
children = generateSchemaJsonFromProps(prop.items.properties, depth + 1, maxDepth);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Handle objects - get children from properties
|
|
607
|
+
if (prop.type === 'object' && prop.properties) {
|
|
608
|
+
children = generateSchemaJsonFromProps(prop.properties, depth + 1, maxDepth);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
name: propName,
|
|
613
|
+
type: propType,
|
|
614
|
+
description: propDesc,
|
|
615
|
+
...(children.length > 0 && { children })
|
|
616
|
+
};
|
|
617
|
+
}).filter(Boolean);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Sorts an array of schema field objects using the standard field priority:
|
|
622
|
+
* 1. Exact 'id' and 'name' fields
|
|
623
|
+
* 2. Fields ending with '_id'
|
|
624
|
+
* 3. Fields ending with '_name'
|
|
625
|
+
* 4. All other fields (alphabetical)
|
|
626
|
+
* @param {Array} fields - Array of schema field objects with 'name' property
|
|
627
|
+
* @returns {Array} Sorted array of schema field objects
|
|
628
|
+
*/
|
|
629
|
+
export function sortSchemaFields(fields) {
|
|
630
|
+
if (!fields || fields.length === 0) return [];
|
|
631
|
+
|
|
632
|
+
const exactIdName = fields.filter(f => f.name === 'id' || f.name === 'name');
|
|
633
|
+
const idSuffix = fields.filter(f => f.name !== 'id' && f.name.endsWith('_id'));
|
|
634
|
+
const nameSuffix = fields.filter(f => f.name !== 'name' && f.name.endsWith('_name'));
|
|
635
|
+
const others = fields.filter(f =>
|
|
636
|
+
!exactIdName.includes(f) &&
|
|
637
|
+
!idSuffix.includes(f) &&
|
|
638
|
+
!nameSuffix.includes(f)
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return [
|
|
642
|
+
...exactIdName.sort((a, b) => a.name.localeCompare(b.name)),
|
|
643
|
+
...idSuffix.sort((a, b) => a.name.localeCompare(b.name)),
|
|
644
|
+
...nameSuffix.sort((a, b) => a.name.localeCompare(b.name)),
|
|
645
|
+
...others.sort((a, b) => a.name.localeCompare(b.name)),
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
|
|
572
649
|
function getHttpOperationParams(dereferencedAPI, path, httpVerb) {
|
|
573
650
|
const requiredParams = {};
|
|
574
651
|
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';
|
|
@@ -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
|
+
|
|
@@ -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,19 @@ 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
|
+
) {
|
|
30
|
+
// Generate each section of the documentation (v2 uses SchemaTable for fields)
|
|
31
|
+
const overviewContent = createOverviewSectionv2(resource.name, resource.type, resource.description, providerName, serviceName);
|
|
32
|
+
const fieldsContent = createFieldsSectionv2(resource.type, resource.resourceData, resource.dereferencedAPI);
|
|
33
|
+
const methodsContent = resource.type === 'Resource' ? createMethodsSection(resource.resourceData, resource.dereferencedAPI) : '';
|
|
34
|
+
const paramsContent = resource.type === 'Resource' ? createParamsSection(resource.resourceData, resource.dereferencedAPI) : '';
|
|
35
|
+
const examplesContent = resource.type === 'Resource' ? createExamplesSection(providerName, serviceName, resource.name, resource.resourceData, resource.dereferencedAPI) : '';
|
|
36
|
+
|
|
37
|
+
// Combine all sections into the final content
|
|
38
|
+
return `${overviewContent}${fieldsContent}${methodsContent}${paramsContent}${examplesContent}`;
|
|
39
|
+
}
|