@stackql/provider-utils 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -154,7 +154,9 @@ components:
154
154
  ### 3. Run the Test
155
155
 
156
156
  ```bash
157
- node tests/docgen/test-docgen.js
157
+ node tests/docgen/test-docgen.js snowflake
158
+ node tests/docgen/test-docgen.js google
159
+ node tests/docgen/test-docgen.js homebrew
158
160
  ```
159
161
 
160
162
  ## Using the Documentation Generator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackql/provider-utils",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -35,7 +35,7 @@
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
37
  "@apidevtools/swagger-parser": "^10.1.1",
38
- "@stackql/deno-openapi-dereferencer": "npm:@jsr/stackql__deno-openapi-dereferencer@^0.3.0",
38
+ "@stackql/deno-openapi-dereferencer": "npm:@jsr/stackql__deno-openapi-dereferencer@^0.3.1",
39
39
  "js-yaml": "^4.1.0",
40
40
  "pluralize": "^8.0.0"
41
41
  },
@@ -13,6 +13,7 @@ export async function generateDocs(options) {
13
13
  providerDir, // e.g., 'output/src/heroku/v00.00.00000'
14
14
  outputDir, // e.g., 'website'
15
15
  providerDataDir, // e.g., 'config/provider-data'
16
+ dereferenced = false,
16
17
  } = options;
17
18
 
18
19
  console.log(`documenting ${providerName}...`);
@@ -60,7 +61,7 @@ export async function generateDocs(options) {
60
61
  const filePath = path.join(serviceDir, file);
61
62
  totalServicesCount++;
62
63
  const serviceFolder = `${servicesDir}/${serviceName}`;
63
- await createDocsForService(filePath, providerName, serviceName, serviceFolder);
64
+ await createDocsForService(filePath, providerName, serviceName, serviceFolder, dereferenced);
64
65
  }
65
66
 
66
67
  console.log(`Processed ${totalServicesCount} services`);
@@ -116,7 +117,7 @@ ${servicesToMarkdown(providerName, secondColumnServices)}
116
117
  }
117
118
 
118
119
  // Process each service sequentially
119
- async function createDocsForService(yamlFilePath, providerName, serviceName, serviceFolder) {
120
+ async function createDocsForService(yamlFilePath, providerName, serviceName, serviceFolder, dereferenced = false) {
120
121
 
121
122
  const data = yaml.load(fs.readFileSync(yamlFilePath, 'utf8'));
122
123
 
@@ -126,12 +127,17 @@ async function createDocsForService(yamlFilePath, providerName, serviceName, ser
126
127
  const ignorePaths = ["$.components.x-stackQL-resources"];
127
128
  let dereferencedAPI;
128
129
 
129
- try {
130
- // dereferencedAPI = await deno_openapi_dereferencer.dereferenceApi(api, "$", ignorePaths);
131
- dereferencedAPI = await SwaggerParser.dereference(api);
132
- dereferencedAPI = await deno_openapi_dereferencer.flattenAllOf(dereferencedAPI);
133
- } catch (error) {
134
- console.error("error in dereferencing or flattening:", error);
130
+ if (dereferenced) {
131
+ // If API is already dereferenced, just use it as is
132
+ dereferencedAPI = api;
133
+ } else {
134
+ try {
135
+ // Only dereference and flatten if needed
136
+ dereferencedAPI = await SwaggerParser.dereference(api);
137
+ dereferencedAPI = await deno_openapi_dereferencer.flattenAllOf(dereferencedAPI);
138
+ } catch (error) {
139
+ console.error("error in dereferencing or flattening:", error);
140
+ }
135
141
  }
136
142
 
137
143
  // Create service directory
@@ -154,11 +160,21 @@ async function createDocsForService(yamlFilePath, providerName, serviceName, ser
154
160
  console.warn(`No 'id' defined for resource: ${resourceName} in service: ${serviceName}`);
155
161
  continue;
156
162
  }
157
-
163
+
164
+ const resourceDescription = resourceData.description || '';
165
+
166
+ // Determine if it's a View or a Resource
167
+ let resourceType = "Resource"; // Default type
168
+ if (resourceData.config?.views?.select) {
169
+ resourceType = "View";
170
+ }
171
+
158
172
  resources.push({
159
173
  name: resourceName,
174
+ description: resourceDescription,
175
+ type: resourceType,
160
176
  resourceData,
161
- dereferencedAPI
177
+ dereferencedAPI,
162
178
  });
163
179
  }
164
180
 
@@ -197,9 +213,7 @@ async function processResource(providerName, serviceFolder, serviceName, resourc
197
213
  const resourceIndexContent = await createResourceIndexContent(
198
214
  providerName,
199
215
  serviceName,
200
- resource.name,
201
- resource.resourceData,
202
- resource.dereferencedAPI,
216
+ resource,
203
217
  );
204
218
  fs.writeFileSync(resourceIndexPath, resourceIndexContent);
205
219
 
@@ -405,18 +405,64 @@ function getHttpRespBody(schema, objectKey) {
405
405
  respDescription: schema.items.description || '',
406
406
  }
407
407
  } else if (schema.type === 'object') {
408
- return {
409
- respProps: schema.properties || {},
410
- respDescription: schema.description || '',
411
- };
408
+ if(objectKey){
409
+ // if objectKey contains [*] print something
410
+ if (objectKey.includes('[*]')) {
411
+ // complex object key
412
+ console.log(`Complex Object Key : ${objectKey}`);
413
+ const parts = objectKey.split('[*]');
414
+ const complexObjectKey = parts[1].replace('.', '');
415
+ console.log(`Item of Interest : ${complexObjectKey}`);
416
+
417
+ // Safe access to respProps
418
+ const respProps = schema?.properties?.items?.additionalProperties?.properties?.[complexObjectKey]?.items?.properties ?? {};
419
+
420
+ // Safe access to respDescription with fallbacks
421
+ const respDescription =
422
+ schema?.properties?.items?.additionalProperties?.properties?.[complexObjectKey]?.items?.description ??
423
+ schema?.properties?.items?.description ??
424
+ '';
425
+
426
+ // console.info(respProps);
427
+ // console.log(respDescription);
428
+ return {
429
+ respProps: respProps,
430
+ respDescription: respDescription,
431
+ };
432
+
433
+ } else {
434
+ // simple object key
435
+ console.log(`Simple Object Key : ${objectKey}`);
436
+ const simpleObjectKey = objectKey.replace('$.', '');
437
+
438
+
439
+ const respProps = (schema?.properties?.[simpleObjectKey]?.items?.properties) ??
440
+ (schema?.properties?.[simpleObjectKey]?.properties) ??
441
+ {};
442
+
443
+ const respDescription = (schema?.properties?.[simpleObjectKey]?.items?.description) ??
444
+ (schema?.description) ??
445
+ '';
446
+
447
+ // console.info(respProps);
448
+ // console.log(respDescription);
449
+ return {
450
+ respProps: respProps,
451
+ respDescription: respDescription,
452
+ };
453
+ }
454
+ } else {
455
+ return {
456
+ respProps: schema.properties || {},
457
+ respDescription: schema.description || '',
458
+ };
459
+ }
412
460
  } else {
413
461
  return {
414
462
  respProps: {},
415
463
  respDescription: '',
416
464
  };
417
465
  }
418
-
419
-
420
466
  }
421
467
 
422
468
  function getHttpOperationParams(dereferencedAPI, path, httpVerb) {
@@ -3,48 +3,54 @@ import {
3
3
  getSqlMethodsWithOrderedFields,
4
4
  sanitizeHtml,
5
5
  } from '../helpers.js';
6
+ import { docView } from './view.js';
6
7
 
7
8
  const mdCodeAnchor = "`";
8
9
 
9
- export function createFieldsSection(resourceData, dereferencedAPI) {
10
+ export function createFieldsSection(resourceType, resourceData, dereferencedAPI) {
10
11
  let content = '## Fields\n\n';
11
12
 
12
- content += 'The following fields are returned by `SELECT` queries:\n\n';
13
-
14
- // Use the reusable function to get methods with ordered fields
15
- const methods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'select');
13
+ if(resourceType === 'Resource'){
16
14
 
17
- if (Object.keys(methods).length > 0) {
18
- // Create the tabs and markdown content
19
- const methodNames = Object.keys(methods);
20
-
21
- // Create the tab values array for the Tabs component
22
- const tabValues = methodNames.map(methodName => {
23
- return `{ label: '${methodName}', value: '${methodName}' }`;
24
- }).join(',\n ');
15
+ content += 'The following fields are returned by `SELECT` queries:\n\n';
16
+
17
+ // Use the reusable function to get methods with ordered fields
18
+ const methods = getSqlMethodsWithOrderedFields(resourceData, dereferencedAPI, 'select');
25
19
 
26
- // Start building the Tabs component
27
- content += `<Tabs
20
+ if (Object.keys(methods).length > 0) {
21
+ // Create the tabs and markdown content
22
+ const methodNames = Object.keys(methods);
23
+
24
+ // Create the tab values array for the Tabs component
25
+ const tabValues = methodNames.map(methodName => {
26
+ return `{ label: '${methodName}', value: '${methodName}' }`;
27
+ }).join(',\n ');
28
+
29
+ // Start building the Tabs component
30
+ content += `<Tabs
28
31
  defaultValue="${methodNames[0]}"
29
32
  values={[
30
33
  ${tabValues}
31
34
  ]}
32
35
  >\n`;
33
36
 
34
- // Create the TabItems with table content
35
- for (const methodName of methodNames) {
36
- const methodData = methods[methodName];
37
-
38
- // Start the TabItem
39
- content += `<TabItem value="${methodName}">\n\n`;
40
-
41
- // Add the method description if available
42
- if (methodData.respDescription && methodData.respDescription.trim().toUpperCase() !== 'OK') {
43
- content += `${sanitizeHtml(methodData.respDescription)}\n\n`;
44
- }
45
-
46
- // Add the table header
47
- content += `<table>
37
+ // Create the TabItems with table content
38
+ for (const methodName of methodNames) {
39
+ const methodData = methods[methodName];
40
+
41
+ // Start the TabItem
42
+ content += `<TabItem value="${methodName}">\n\n`;
43
+
44
+ // Add the method description if available
45
+ if (methodData.respDescription
46
+ && methodData.respDescription.trim().toUpperCase() !== 'OK'
47
+ && methodData.respDescription.trim() !== 'Successful response'
48
+ ) {
49
+ content += `${sanitizeHtml(methodData.respDescription)}\n\n`;
50
+ }
51
+
52
+ // Add the table header
53
+ content += `<table>
48
54
  <thead>
49
55
  <tr>
50
56
  <th>Name</th>
@@ -54,28 +60,34 @@ export function createFieldsSection(resourceData, dereferencedAPI) {
54
60
  </thead>
55
61
  <tbody>`;
56
62
 
57
- // Add each property as a row in the table
58
- for (const [propName, propData] of Object.entries(methodData.properties)) {
59
- content += `\n<tr>
63
+ // Add each property as a row in the table
64
+ for (const [propName, propData] of Object.entries(methodData.properties)) {
65
+ content += `\n<tr>
60
66
  <td><CopyableCode code="${propName}" /></td>
61
67
  <td><code>${propData.type}</code></td>
62
68
  <td>${sanitizeHtml(propData.description)}</td>
63
69
  </tr>`;
64
- }
70
+ }
65
71
 
66
- content += `\n</tbody>
72
+ content += `\n</tbody>
67
73
  </table>
68
74
  `;
69
75
 
70
- // Close the TabItem
71
- content += `</TabItem>\n`;
76
+ // Close the TabItem
77
+ content += `</TabItem>\n`;
78
+ }
79
+
80
+ // Close the Tabs component
81
+ content += `</Tabs>\n`;
82
+ } else {
83
+ // no fields
84
+ content += `${mdCodeAnchor}SELECT${mdCodeAnchor} not supported for this resource, use ${mdCodeAnchor}SHOW METHODS${mdCodeAnchor} to view available operations for the resource.\n\n`;
72
85
  }
73
-
74
- // Close the Tabs component
75
- content += `</Tabs>\n`;
86
+
76
87
  } else {
77
- // no fields
78
- content += `${mdCodeAnchor}SELECT${mdCodeAnchor} not supported for this resource, use ${mdCodeAnchor}SHOW METHODS${mdCodeAnchor} to view available operations for the resource.\n\n`;
88
+ // its a view
89
+ console.log(`processing view : ${resourceData.name}...`)
90
+ content += docView(resourceData);
79
91
  }
80
92
 
81
93
  return content;
@@ -3,7 +3,7 @@ import {
3
3
  getIndefiniteArticle,
4
4
  } from '../helpers.js';
5
5
 
6
- export function createOverviewSection(resourceName, providerName, serviceName) {
6
+ export function createOverviewSection(resourceName, resourceType, resourceDescription, providerName, serviceName) {
7
7
 
8
8
  let content = `---
9
9
  title: ${resourceName}
@@ -25,12 +25,16 @@ import CopyableCode from '@site/src/components/CopyableCode/CopyableCode';
25
25
  import Tabs from '@theme/Tabs';
26
26
  import TabItem from '@theme/TabItem';
27
27
 
28
- Creates, updates, deletes, gets or lists ${getIndefiniteArticle(resourceName)} <code>${resourceName}</code> resource.
28
+ `;
29
+
30
+ content += resourceDescription ? resourceDescription : `Creates, updates, deletes, gets or lists ${getIndefiniteArticle(resourceName)} <code>${resourceName}</code> resource.`;
31
+
32
+ content += `
29
33
 
30
34
  ## Overview
31
35
  <table><tbody>
32
36
  <tr><td><b>Name</b></td><td><code>${resourceName}</code></td></tr>
33
- <tr><td><b>Type</b></td><td>Resource</td></tr>
37
+ <tr><td><b>Type</b></td><td>${resourceType}</td></tr>
34
38
  <tr><td><b>Id</b></td><td><CopyableCode code="${providerName}.${serviceName}.${resourceName}" /></td></tr>
35
39
  </tbody></table>
36
40
 
@@ -0,0 +1,130 @@
1
+ // src/docgen/resource/view.js
2
+ export function docView(resourceData) {
3
+
4
+ let content = '';
5
+
6
+ const fields = resourceData.config?.views?.fields ?? [];
7
+
8
+ if (fields.length === 0) {
9
+ content += `See the SQL Definition (view DDL) for fields returned by this view.\n\n`;
10
+ } else {
11
+ // Add the table
12
+ content += `The following fields are returned by this view:\n\n`;
13
+ content += `<table>
14
+ <thead>
15
+ <tr>
16
+ <th>Name</th>
17
+ <th>Datatype</th>
18
+ <th>Description</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>`;
22
+
23
+ for (const field of fields) {
24
+ content += `
25
+ <tr>
26
+ <td>${field.name}</td>
27
+ <td>${field.type}</td>
28
+ <td>${field.description}</td>
29
+ </tr>`;
30
+ }
31
+
32
+ // Close the table
33
+ content += `
34
+ </tbody>
35
+ </table>\n\n`;
36
+ }
37
+
38
+ // Add required params section if exists
39
+ const requiredParams = resourceData.config?.views?.requiredParams ?? [];
40
+ if (requiredParams.length > 0) {
41
+ // add the table
42
+ content += `## Required Parameters\n\n`;
43
+ content += `The following parameters are required by this view:\n\n`;
44
+ content += `<table>
45
+ <thead>
46
+ <tr>
47
+ <th>Name</th>
48
+ <th>Datatype</th>
49
+ <th>Description</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>`;
53
+
54
+ for (const param of requiredParams) {
55
+ content += `
56
+ <tr>
57
+ <td>${param.name}</td>
58
+ <td>${param.type}</td>
59
+ <td>${param.description}</td>
60
+ </tr>`;
61
+ }
62
+
63
+ // Close the table
64
+ content += `
65
+ </tbody>
66
+ </table>\n\n`;
67
+ }
68
+
69
+ // SQL Definition section
70
+ content += `## SQL Definition\n\n`;
71
+
72
+ // Build array of dialect objects
73
+ const dialects = [];
74
+ let currentSelect = resourceData.config.views.select;
75
+
76
+ // Add primary dialect
77
+ dialects.push({
78
+ name: extractDialectName(currentSelect.predicate),
79
+ ddl: currentSelect.ddl
80
+ });
81
+
82
+ // Add fallback dialects
83
+ while (currentSelect.fallback) {
84
+ currentSelect = currentSelect.fallback;
85
+ dialects.push({
86
+ name: extractDialectName(currentSelect.predicate),
87
+ ddl: currentSelect.ddl
88
+ });
89
+ }
90
+
91
+ // Create the tabbed interface
92
+ const tabValues = dialects.map(dialect => (
93
+ `{ label: '${dialect.name}', value: '${dialect.name}' }`
94
+ )).join(',\n');
95
+
96
+ content += `<Tabs
97
+ defaultValue="${dialects[0].name}"
98
+ values={[
99
+ ${tabValues}
100
+ ]}
101
+ >\n`;
102
+
103
+ // Create tab content
104
+ for (const dialect of dialects) {
105
+ content += `<TabItem value="${dialect.name}">\n\n`;
106
+ content += `\`\`\`sql
107
+ ${dialect.ddl}
108
+ \`\`\`\n\n`;
109
+ content += `</TabItem>\n`;
110
+ }
111
+
112
+ content += `</Tabs>\n`;
113
+
114
+ return content;
115
+
116
+ }
117
+
118
+ // Extract and format dialect name from predicate
119
+ function extractDialectName(predicate) {
120
+ if (!predicate) {
121
+ return 'Default';
122
+ }
123
+ const dialectMatch = predicate.match(/sqlDialect\s*==\s*['"](.*?)['"]/);
124
+ if (!dialectMatch || !dialectMatch[1]) {
125
+ throw new Error(`Invalid dialect predicate: ${predicate}`);
126
+ }
127
+
128
+ // Capitalize first letter of dialect name
129
+ return dialectMatch[1].charAt(0).toUpperCase() + dialectMatch[1].slice(1);
130
+ }
@@ -9,16 +9,14 @@ import { createExamplesSection } from './resource/examples.js';
9
9
  export async function createResourceIndexContent(
10
10
  providerName,
11
11
  serviceName,
12
- resourceName,
13
- resourceData,
14
- dereferencedAPI,
12
+ resource,
15
13
  ) {
16
14
  // Generate each section of the documentation
17
- const overviewContent = createOverviewSection(resourceName, providerName, serviceName);
18
- const fieldsContent = createFieldsSection(resourceData, dereferencedAPI);
19
- const methodsContent = createMethodsSection(resourceData, dereferencedAPI);
20
- const paramsContent = createParamsSection(resourceData, dereferencedAPI);
21
- const examplesContent = createExamplesSection(providerName, serviceName, resourceName, resourceData, dereferencedAPI);
15
+ const overviewContent = createOverviewSection(resource.name, resource.type, resource.description, providerName, serviceName);
16
+ const fieldsContent = createFieldsSection(resource.type, resource.resourceData, resource.dereferencedAPI);
17
+ const methodsContent = resource.type === 'Resource' ? createMethodsSection(resource.resourceData, resource.dereferencedAPI) : '';
18
+ const paramsContent = resource.type === 'Resource' ? createParamsSection(resource.resourceData, resource.dereferencedAPI) : '';
19
+ const examplesContent = resource.type === 'Resource' ? createExamplesSection(providerName, serviceName, resource.name, resource.resourceData, resource.dereferencedAPI) : '';
22
20
 
23
21
  // Combine all sections into the final content
24
22
  return `${overviewContent}${fieldsContent}${methodsContent}${paramsContent}${examplesContent}`;