@stackql/provider-utils 0.2.3 → 0.3.0

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,9 +154,16 @@ 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
 
162
+
163
+ node tests/providerdev/test-split.js okta tests/providerdev/split-source/okta/management-minimal.yaml path
164
+ node tests/providerdev/test-analyze.js okta
165
+ node tests/providerdev/test-generate.js okta
166
+
160
167
  ## Using the Documentation Generator
161
168
 
162
169
  ### Basic Example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackql/provider-utils",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -36,6 +36,7 @@
36
36
  "dependencies": {
37
37
  "@apidevtools/swagger-parser": "^10.1.1",
38
38
  "@stackql/deno-openapi-dereferencer": "npm:@jsr/stackql__deno-openapi-dereferencer@^0.3.1",
39
+ "csv-parser": "^3.2.0",
39
40
  "js-yaml": "^4.1.0",
40
41
  "pluralize": "^8.0.0"
41
42
  },
@@ -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
 
@@ -397,28 +397,6 @@ function getHttpOperationInfo(dereferencedAPI, path, httpVerb, mediaType, openAP
397
397
  };
398
398
  }
399
399
 
400
- // function getHttpRespBody(schema, objectKey) {
401
-
402
- // if (schema.type === 'array') {
403
- // return {
404
- // respProps: schema.items.properties || {},
405
- // respDescription: schema.items.description || '',
406
- // }
407
- // } else if (schema.type === 'object') {
408
- // return {
409
- // respProps: schema.properties || {},
410
- // respDescription: schema.description || '',
411
- // };
412
- // } else {
413
- // return {
414
- // respProps: {},
415
- // respDescription: '',
416
- // };
417
- // }
418
-
419
-
420
- // }
421
-
422
400
  function getHttpRespBody(schema, objectKey) {
423
401
 
424
402
  if (schema.type === 'array') {
@@ -435,12 +413,20 @@ function getHttpRespBody(schema, objectKey) {
435
413
  const parts = objectKey.split('[*]');
436
414
  const complexObjectKey = parts[1].replace('.', '');
437
415
  console.log(`Item of Interest : ${complexObjectKey}`);
438
- const respProps = schema.properties.items.additionalProperties.properties[complexObjectKey].items.properties;
439
- console.info(respProps);
440
- const respDescription = schema.properties.items.additionalProperties.properties[complexObjectKey].items.description || schema.properties.items.description || '';
441
- console.log(respDescription);
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);
442
428
  return {
443
- respProps: respProps || {},
429
+ respProps: respProps,
444
430
  respDescription: respDescription,
445
431
  };
446
432
 
@@ -448,12 +434,20 @@ function getHttpRespBody(schema, objectKey) {
448
434
  // simple object key
449
435
  console.log(`Simple Object Key : ${objectKey}`);
450
436
  const simpleObjectKey = objectKey.replace('$.', '');
451
- const respProps = schema.properties[simpleObjectKey].items.properties;
452
- console.info(respProps);
453
- const respDescription = schema.properties[simpleObjectKey].items.description || schema.description || '';
454
- console.log(respDescription);
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);
455
449
  return {
456
- respProps: respProps || {},
450
+ respProps: respProps,
457
451
  respDescription: respDescription,
458
452
  };
459
453
  }
@@ -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}`;
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
- // src/index.js
1
+ // @stackql/provider-utils/src/index.js
2
2
 
3
3
  // Main entry point for Node.js
4
- export * as docgen from './docgen/index.js';
4
+ export * as docgen from './docgen/index.js';
5
+ export * as providerdev from './providerdev/index.js';
package/src/logger.js ADDED
@@ -0,0 +1,52 @@
1
+ // @stackql/provider-utils/src/logger.js
2
+
3
+ // Simple logger implementation
4
+ const logLevels = {
5
+ error: 0,
6
+ warn: 1,
7
+ info: 2,
8
+ debug: 3
9
+ };
10
+
11
+ let currentLevel = 'info';
12
+
13
+ const logger = {
14
+ get level() {
15
+ return currentLevel;
16
+ },
17
+
18
+ set level(newLevel) {
19
+ if (logLevels[newLevel] !== undefined) {
20
+ currentLevel = newLevel;
21
+ } else {
22
+ console.warn(`Invalid log level: ${newLevel}. Using 'info' instead.`);
23
+ currentLevel = 'info';
24
+ }
25
+ },
26
+
27
+ error: (message) => {
28
+ if (logLevels[currentLevel] >= logLevels.error) {
29
+ console.error(`ERROR: ${message}`);
30
+ }
31
+ },
32
+
33
+ warn: (message) => {
34
+ if (logLevels[currentLevel] >= logLevels.warn) {
35
+ console.warn(`WARNING: ${message}`);
36
+ }
37
+ },
38
+
39
+ info: (message) => {
40
+ if (logLevels[currentLevel] >= logLevels.info) {
41
+ console.info(`INFO: ${message}`);
42
+ }
43
+ },
44
+
45
+ debug: (message) => {
46
+ if (logLevels[currentLevel] >= logLevels.debug) {
47
+ console.debug(`DEBUG: ${message}`);
48
+ }
49
+ }
50
+ };
51
+
52
+ export default logger;