@stackql/provider-utils 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 StackQL Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # StackQL Provider Utils
2
+
3
+ A comprehensive toolkit for transforming OpenAPI specs into StackQL providers. Includes parsing, mapping, validation, testing, and documentation generation utilities.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Prerequisites](#prerequisites)
8
+ - [Installation](#installation)
9
+ - [Local Development Setup](#local-development-setup)
10
+ - [Testing with Node.js](#testing-with-nodejs)
11
+ - [Using the Documentation Generator](#using-the-documentation-generator)
12
+ - [API Reference](#api-reference)
13
+ - [Contributing](#contributing)
14
+
15
+ ## Prerequisites
16
+
17
+ ### For Node.js
18
+ - Node.js >= 20
19
+ - npm or yarn
20
+ - StackQL server (for documentation generation)
21
+
22
+ ### Installing StackQL
23
+
24
+ Download and install StackQL from [stackql.io/downloads](https://stackql.io/downloads)
25
+
26
+ ```bash
27
+ # macOS
28
+ brew install stackql
29
+
30
+ # Linux
31
+ curl -L https://bit.ly/stackql-zip -O && unzip stackql-zip
32
+
33
+ # Windows
34
+ # Download from https://stackql.io/downloads
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ### For Node.js Projects
40
+
41
+ ```bash
42
+ npm install @stackql/provider-utils
43
+ # or
44
+ yarn add @stackql/provider-utils
45
+ ```
46
+
47
+ ## Local Development Setup
48
+
49
+ 1. Clone the repository:
50
+ ```bash
51
+ git clone https://github.com/stackql/stackql-provider-utils.git
52
+ cd stackql-provider-utils
53
+ ```
54
+
55
+ 2. Install dependencies (Node.js):
56
+ ```bash
57
+ npm install
58
+ ```
59
+
60
+ ## Testing with Node.js
61
+
62
+ ### 1. Create a Test Script
63
+
64
+ Create a file `test-docgen.js`:
65
+
66
+ ```javascript
67
+ import { docgen } from './src/index.js';
68
+
69
+ // Test the documentation generator
70
+ async function testDocGen() {
71
+ try {
72
+ const result = await docgen.generateDocs({
73
+ providerName: 'myservice',
74
+ providerDir: './test-data/output/src/myservice/v00.00.00000',
75
+ outputDir: './test-output',
76
+ providerDataDir: './test-data/provider-data',
77
+ stackqlConfig: {
78
+ host: 'localhost',
79
+ port: 5444,
80
+ user: 'stackql',
81
+ database: 'stackql'
82
+ }
83
+ });
84
+
85
+ console.log('Documentation generated successfully:', result);
86
+ } catch (error) {
87
+ console.error('Error generating documentation:', error);
88
+ }
89
+ }
90
+
91
+ testDocGen();
92
+ ```
93
+
94
+ ### 2. Set Up Test Data
95
+
96
+ Create the required directory structure:
97
+
98
+ ```bash
99
+ mkdir -p test-data/output/src/myservice/v00.00.00000/services
100
+ mkdir -p test-data/provider-data
101
+ ```
102
+
103
+ Add test files:
104
+
105
+ `test-data/provider-data/headerContent1.txt`:
106
+ ```
107
+ ---
108
+ title: myservice
109
+ hide_title: false
110
+ hide_table_of_contents: false
111
+ keywords:
112
+ - myservice
113
+ - stackql
114
+ - infrastructure-as-code
115
+ - configuration-as-data
116
+ description: Query and manage myservice resources using SQL
117
+ ---
118
+
119
+ # myservice Provider
120
+
121
+ The myservice provider for StackQL allows you to query, deploy, and manage myservice resources using SQL.
122
+ ```
123
+
124
+ `test-data/provider-data/headerContent2.txt`:
125
+ ```
126
+ See the [myservice provider documentation](https://myservice.com/docs) for more information.
127
+ ```
128
+
129
+ `test-data/output/src/myservice/v00.00.00000/services/example.yaml`:
130
+ ```yaml
131
+ openapi: 3.0.0
132
+ info:
133
+ title: Example Service
134
+ version: 1.0.0
135
+ paths:
136
+ /examples:
137
+ get:
138
+ operationId: listExamples
139
+ responses:
140
+ '200':
141
+ description: Success
142
+ components:
143
+ x-stackQL-resources:
144
+ examples:
145
+ id: myservice.example.examples
146
+ name: examples
147
+ title: Examples
148
+ methods:
149
+ list:
150
+ operation:
151
+ $ref: '#/paths/~1examples/get'
152
+ response:
153
+ mediaType: application/json
154
+ openAPIDocKey: '200'
155
+ sqlVerbs:
156
+ select:
157
+ - $ref: '#/components/x-stackQL-resources/examples/methods/list'
158
+ ```
159
+
160
+ ### 3. Start StackQL Server
161
+
162
+ ```bash
163
+ # In a separate terminal
164
+ stackql srv \
165
+ --pgsrv.port=5444 \
166
+ --pgsrv.tls=false \
167
+ --loglevel=INFO
168
+ ```
169
+
170
+ ### 4. Run the Test
171
+
172
+ ```bash
173
+ sh start-stackql-server.sh tests/docgen
174
+ node tests/docgen/test-docgen.js
175
+ ```
176
+
177
+ ## Using the Documentation Generator
178
+
179
+ ### Basic Example
180
+
181
+ ```javascript
182
+ import { docgen } from '@stackql/provider-utils';
183
+
184
+ const options = {
185
+ providerName: 'github',
186
+ providerDir: './output/src/github/v00.00.00000',
187
+ outputDir: './docs',
188
+ providerDataDir: './config/provider-data',
189
+ stackqlConfig: {
190
+ host: 'localhost',
191
+ port: 5444,
192
+ user: 'stackql',
193
+ database: 'stackql'
194
+ }
195
+ };
196
+
197
+ const result = await docgen.generateDocs(options);
198
+ console.log(`Generated docs for ${result.totalServices} services and ${result.totalResources} resources`);
199
+ console.log(`Output location: ${result.outputPath}`);
200
+ ```
201
+
202
+ ### Options
203
+
204
+ | Option | Type | Description | Default |
205
+ |--------|------|-------------|---------|
206
+ | `providerName` | string | Name of the provider (e.g., 'github', 'aws') | Required |
207
+ | `providerDir` | string | Path to provider spec directory | Required |
208
+ | `outputDir` | string | Directory for generated documentation | Required |
209
+ | `providerDataDir` | string | Directory containing provider header files | Required |
210
+ | `stackqlConfig` | object | StackQL server connection configuration | See below |
211
+
212
+ #### StackQL Config Options
213
+
214
+ ```javascript
215
+ {
216
+ host: 'localhost', // StackQL server host
217
+ port: 5444, // StackQL server port
218
+ user: 'stackql', // Database user
219
+ database: 'stackql' // Database name
220
+ }
221
+ ```
222
+
223
+ ## Directory Structure Requirements
224
+
225
+ ### Provider Data Directory
226
+ ```
227
+ provider-data/
228
+ ├── headerContent1.txt # Provider introduction
229
+ ├── headerContent2.txt # Additional provider info
230
+ └── stackql-provider-registry.mdx (optional)
231
+ ```
232
+
233
+ ### Provider Spec Directory
234
+ ```
235
+ output/src/{provider}/v00.00.00000/
236
+ ├── provider.yaml
237
+ └── services/
238
+ ├── service1.yaml
239
+ ├── service2.yaml
240
+ └── ...
241
+ ```
242
+
243
+ ### Generated Output
244
+ ```
245
+ docs/{provider}-docs/
246
+ ├── index.md
247
+ ├── stackql-provider-registry.mdx
248
+ └── providers/
249
+ └── {provider}/
250
+ └── {service}/
251
+ ├── index.md
252
+ └── {resource}/
253
+ └── index.md
254
+ ```
255
+
256
+ ## Troubleshooting
257
+
258
+ ### StackQL Server Connection Issues
259
+ - Ensure StackQL server is running: `stackql srv --pgsrv.port=5444`
260
+ - Check if port 5444 is available
261
+ - Verify connection settings in `stackqlConfig`
262
+
263
+ ### Missing Provider Data
264
+ - Ensure `headerContent1.txt` and `headerContent2.txt` exist in provider data directory
265
+ - Check file permissions
266
+
267
+ ### Empty Documentation
268
+ - Verify provider specs have `x-stackQL-resources` components
269
+ - Check that resources have proper method definitions
270
+
271
+ ## API Reference
272
+
273
+ ### `docgen.generateDocs(options)`
274
+
275
+ Generates documentation for a StackQL provider.
276
+
277
+ **Parameters:**
278
+ - `options` (Object): Configuration options
279
+
280
+ **Returns:**
281
+ - Promise<Object>: Result object containing:
282
+ - `totalServices`: Number of services processed
283
+ - `totalResources`: Number of resources documented
284
+ - `outputPath`: Path to generated documentation
285
+
286
+ **Example:**
287
+ ```javascript
288
+ const result = await docgen.generateDocs({
289
+ providerName: 'aws',
290
+ providerDir: './providers/src/aws/v00.00.00000',
291
+ outputDir: './documentation',
292
+ providerDataDir: './config/aws',
293
+ stackqlConfig: {
294
+ host: 'localhost',
295
+ port: 5444
296
+ }
297
+ });
298
+ ```
299
+
300
+ ### `docgen.createResourceIndexContent(...)`
301
+
302
+ Creates markdown content for a single resource. This is a lower-level function used internally by `generateDocs`.
303
+
304
+ ## Contributing
305
+
306
+ 1. Fork the repository
307
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
308
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
309
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
310
+ 5. Open a Pull Request
311
+
312
+ ## License
313
+
314
+ MIT
315
+
316
+ ## Support
317
+
318
+ - [StackQL Documentation](https://stackql.io/docs)
319
+ - [GitHub Issues](https://github.com/stackql/stackql-provider-utils/issues)
320
+ - [StackQL Discord](https://discord.gg/stackql)
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@stackql/provider-utils",
3
+ "version": "0.1.0",
4
+ "description": "Utilities for building StackQL providers from OpenAPI specifications.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.js"
10
+ },
11
+ "./docgen": {
12
+ "import": "./src/docgen/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "src/",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "jest",
22
+ "lint": "eslint src"
23
+ },
24
+ "keywords": [
25
+ "stackql",
26
+ "openapi",
27
+ "infrastructure-as-code",
28
+ "sql",
29
+ "cloud",
30
+ "provider",
31
+ "documentation"
32
+ ],
33
+ "author": "StackQL Studios",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@apidevtools/swagger-parser": "^10.1.1",
37
+ "@stackql/deno-openapi-dereferencer": "npm:@jsr/stackql__deno-openapi-dereferencer@^0.3.0",
38
+ "js-yaml": "^4.1.0",
39
+ "pluralize": "^8.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "eslint": "^8.0.0",
43
+ "jest": "^29.0.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=16.0.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/stackql/stackql-provider-utils.git"
51
+ }
52
+ }
@@ -0,0 +1,298 @@
1
+ // src/docgen/generator.js
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import yaml from 'js-yaml';
6
+ import { createResourceIndexContent } from './resource-content.js';
7
+ import SwaggerParser from '@apidevtools/swagger-parser';
8
+ import * as deno_openapi_dereferencer from "@stackql/deno-openapi-dereferencer";
9
+
10
+ export async function generateDocs(options) {
11
+ const {
12
+ providerName,
13
+ providerDir, // e.g., 'output/src/heroku/v00.00.00000'
14
+ outputDir, // e.g., 'docs'
15
+ providerDataDir, // e.g., 'config/provider-data'
16
+ // stackqlConfig = {
17
+ // host: 'localhost',
18
+ // port: 5444,
19
+ // user: 'stackql',
20
+ // database: 'stackql'
21
+ // }
22
+ } = options;
23
+
24
+ console.log(`documenting ${providerName}...`);
25
+
26
+ const docsDir = path.join(outputDir, `${providerName}-docs`);
27
+
28
+ // Clean existing docs
29
+ fs.existsSync(`${docsDir}/index.md`) && fs.unlinkSync(`${docsDir}/index.md`);
30
+ fs.existsSync(`${docsDir}/providers`) && fs.rmSync(`${docsDir}/providers`, { recursive: true, force: true });
31
+
32
+ // Check for provider data files
33
+ console.log(providerDataDir);
34
+ try {
35
+ const files = fs.readdirSync(providerDataDir);
36
+ console.log('Files in providerDataDir:', files);
37
+ } catch (err) {
38
+ console.error('Error reading providerDataDir:', err.message);
39
+ }
40
+
41
+
42
+
43
+
44
+ const headerContent1Path = path.join(providerDataDir, 'headerContent1.txt');
45
+ const headerContent2Path = path.join(providerDataDir, 'headerContent2.txt');
46
+ const mdxPath = path.join(providerDataDir, 'stackql-provider-registry.mdx');
47
+
48
+ if (!fs.existsSync(headerContent1Path) || !fs.existsSync(headerContent2Path)) {
49
+ throw new Error(`Missing headerContent1.txt or headerContent2.txt in ${providerDataDir}`);
50
+ }
51
+
52
+ const headerContent1 = fs.readFileSync(headerContent1Path, 'utf8');
53
+ const headerContent2 = fs.readFileSync(headerContent2Path, 'utf8');
54
+
55
+ // Initialize counters
56
+ let servicesForIndex = [];
57
+ let totalServicesCount = 0;
58
+ let totalResourcesCount = 0;
59
+
60
+ // Process services
61
+ const serviceDir = path.join(providerDir, 'services');
62
+ console.log(`Processing services in ${serviceDir}...`);
63
+ const serviceFiles = fs.readdirSync(serviceDir).filter(file => path.extname(file) === '.yaml');
64
+
65
+ for (const file of serviceFiles) {
66
+ const serviceName = path.basename(file, '.yaml').replace(/-/g, '_');
67
+ console.log(`Processing service: ${serviceName}`);
68
+ servicesForIndex.push(serviceName);
69
+ const filePath = path.join(serviceDir, file);
70
+ totalServicesCount++;
71
+ const serviceFolder = `${docsDir}/providers/${providerName}/${serviceName}`;
72
+ await createDocsForService(filePath, providerName, serviceName, serviceFolder);
73
+ }
74
+
75
+ console.log(`Processed ${totalServicesCount} services`);
76
+
77
+ // Count total resources
78
+ totalResourcesCount = fs.readdirSync(`${docsDir}/providers/${providerName}`, { withFileTypes: true })
79
+ .filter(dirent => dirent.isDirectory())
80
+ .map(dirent => fs.readdirSync(`${docsDir}/providers/${providerName}/${dirent.name}`).length)
81
+ .reduce((a, b) => a + b, 0);
82
+
83
+ console.log(`Processed ${totalResourcesCount} resources`);
84
+
85
+ // Create provider index
86
+ servicesForIndex = [...new Set(servicesForIndex)];
87
+ servicesForIndex.sort();
88
+
89
+ const half = Math.ceil(servicesForIndex.length / 2);
90
+ const firstColumnServices = servicesForIndex.slice(0, half);
91
+ const secondColumnServices = servicesForIndex.slice(half);
92
+
93
+ const indexContent = `${headerContent1}
94
+
95
+ :::info Provider Summary
96
+
97
+ <div class="row">
98
+ <div class="providerDocColumn">
99
+ <span>total services:&nbsp;<b>${totalServicesCount}</b></span><br />
100
+ <span>total resources:&nbsp;<b>${totalResourcesCount}</b></span><br />
101
+ </div>
102
+ </div>
103
+
104
+ :::
105
+
106
+ ${headerContent2}
107
+
108
+ ## Services
109
+ <div class="row">
110
+ <div class="providerDocColumn">
111
+ ${servicesToMarkdown(providerName, firstColumnServices)}
112
+ </div>
113
+ <div class="providerDocColumn">
114
+ ${servicesToMarkdown(providerName, secondColumnServices)}
115
+ </div>
116
+ </div>
117
+ `;
118
+
119
+ // Write index
120
+ const indexPath = path.join(docsDir, 'index.md');
121
+ fs.writeFileSync(indexPath, indexContent);
122
+ console.log(`Index file created at ${indexPath}`);
123
+
124
+ // Copy MDX file if exists
125
+ if (fs.existsSync(mdxPath)) {
126
+ fs.copyFileSync(mdxPath, path.join(docsDir, 'stackql-provider-registry.mdx'));
127
+ console.log(`MDX file copied`);
128
+ }
129
+
130
+ return {
131
+ totalServices: totalServicesCount,
132
+ totalResources: totalResourcesCount,
133
+ outputPath: docsDir
134
+ };
135
+ }
136
+
137
+ // Process each service sequentially
138
+ async function createDocsForService(yamlFilePath, providerName, serviceName, serviceFolder) {
139
+
140
+ const data = yaml.load(fs.readFileSync(yamlFilePath, 'utf8'));
141
+
142
+ // Create a new SwaggerParser instance
143
+ let parser = new SwaggerParser();
144
+ const api = await parser.parse(yamlFilePath);
145
+ const ignorePaths = ["$.components.x-stackQL-resources"];
146
+ let dereferencedAPI;
147
+
148
+ try {
149
+ dereferencedAPI = await deno_openapi_dereferencer.dereferenceApi(api, "$", ignorePaths);
150
+ dereferencedAPI = await deno_openapi_dereferencer.flattenAllOf(dereferencedAPI);
151
+ } catch (error) {
152
+ console.error("error in dereferencing or flattening:", error);
153
+ }
154
+
155
+ // Create service directory
156
+ if (!fs.existsSync(serviceFolder)) {
157
+ fs.mkdirSync(serviceFolder, { recursive: true });
158
+ }
159
+
160
+ const resourcesObj = data.components['x-stackQL-resources'];
161
+
162
+ if (!resourcesObj) {
163
+ console.warn(`No resources found in ${yamlFilePath}`);
164
+ return;
165
+ }
166
+
167
+ const resources = [];
168
+ for (let resourceName in resourcesObj) {
169
+
170
+ let resourceData = resourcesObj[resourceName];
171
+ if (!resourceData.id) {
172
+ console.warn(`No 'id' defined for resource: ${resourceName} in service: ${serviceName}`);
173
+ continue;
174
+ }
175
+
176
+ resources.push({
177
+ name: resourceName,
178
+ resourceData,
179
+ dereferencedAPI
180
+ });
181
+ }
182
+
183
+ // Process service index
184
+ const serviceIndexPath = path.join(serviceFolder, 'index.md');
185
+ const serviceIndexContent = await createServiceIndexContent(providerName, serviceName, resources);
186
+ fs.writeFileSync(serviceIndexPath, serviceIndexContent);
187
+
188
+ // Split into columns and process resources one by one
189
+ const halfLength = Math.ceil(resources.length / 2);
190
+ const firstColumn = resources.slice(0, halfLength);
191
+ const secondColumn = resources.slice(halfLength);
192
+
193
+ // Process each resource in first column
194
+ for (const resource of firstColumn) {
195
+ await processResource(providerName, serviceFolder, serviceName, resource);
196
+ }
197
+
198
+ // Process each resource in second column
199
+ for (const resource of secondColumn) {
200
+ await processResource(providerName, serviceFolder, serviceName, resource);
201
+ }
202
+
203
+ console.log(`Generated documentation for ${serviceName}`);
204
+ }
205
+
206
+ async function processResource(providerName, serviceFolder, serviceName, resource) {
207
+ console.log(`Processing resource: ${resource.name}`);
208
+
209
+ const resourceFolder = path.join(serviceFolder, resource.name);
210
+ if (!fs.existsSync(resourceFolder)) {
211
+ fs.mkdirSync(resourceFolder, { recursive: true });
212
+ }
213
+
214
+ const resourceIndexPath = path.join(resourceFolder, 'index.md');
215
+ const resourceIndexContent = await createResourceIndexContent(
216
+ providerName,
217
+ serviceName,
218
+ resource.name,
219
+ resource.resourceData,
220
+ resource.dereferencedAPI,
221
+ );
222
+ fs.writeFileSync(resourceIndexPath, resourceIndexContent);
223
+
224
+ // After writing the file, force garbage collection if available (optional)
225
+ if (global.gc) {
226
+ global.gc();
227
+ }
228
+ }
229
+
230
+ async function createServiceIndexContent(providerName, serviceName, resources) {
231
+ const totalResources = resources.length; // Calculate the total resources
232
+
233
+ // Sort resources alphabetically by name
234
+ resources.sort((a, b) => a.name.localeCompare(b.name));
235
+
236
+ const halfLength = Math.ceil(totalResources / 2);
237
+ const firstColumnResources = resources.slice(0, halfLength);
238
+ const secondColumnResources = resources.slice(halfLength);
239
+
240
+ // Generate the HTML for resource links in the first column
241
+ const firstColumnLinks = generateResourceLinks(providerName, serviceName, firstColumnResources);
242
+
243
+ // Generate the HTML for resource links in the second column
244
+ const secondColumnLinks = generateResourceLinks(providerName, serviceName, secondColumnResources);
245
+
246
+ // Create the markdown content for the service index
247
+ // You can customize this content as needed
248
+ return `---
249
+ title: ${serviceName}
250
+ hide_title: false
251
+ hide_table_of_contents: false
252
+ keywords:
253
+ - ${serviceName}
254
+ - ${providerName}
255
+ - stackql
256
+ - infrastructure-as-code
257
+ - configuration-as-data
258
+ - cloud inventory
259
+ description: Query, deploy and manage ${providerName} resources using SQL
260
+ custom_edit_url: null
261
+ image: /img/providers/${providerName}/stackql-${providerName}-provider-featured-image.png
262
+ ---
263
+
264
+ ${serviceName} service documentation.
265
+
266
+ :::info Service Summary
267
+
268
+ <div class="row">
269
+ <div class="providerDocColumn">
270
+ <span>total resources:&nbsp;<b>${totalResources}</b></span><br />
271
+ </div>
272
+ </div>
273
+
274
+ :::
275
+
276
+ ## Resources
277
+ <div class="row">
278
+ <div class="providerDocColumn">
279
+ ${firstColumnLinks}
280
+ </div>
281
+ <div class="providerDocColumn">
282
+ ${secondColumnLinks}
283
+ </div>
284
+ </div>`;
285
+ }
286
+
287
+ function generateResourceLinks(providerName, serviceName, resources) {
288
+ // Generate resource links for the service index
289
+ const resourceLinks = resources.map((resource) => {
290
+ return `<a href="/providers/${providerName}/${serviceName}/${resource.name}/">${resource.name}</a>`;
291
+ });
292
+ return resourceLinks.join('<br />\n');
293
+ }
294
+
295
+ // Function to convert services to markdown links
296
+ function servicesToMarkdown(providerName, servicesList) {
297
+ return servicesList.map(service => `<a href="/providers/${providerName}/${service}/">${service}</a><br />`).join('\n');
298
+ }