@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 +8 -1
- package/package.json +2 -1
- package/src/docgen/generator.js +27 -13
- package/src/docgen/helpers.js +26 -32
- package/src/docgen/resource/fields.js +53 -41
- package/src/docgen/resource/overview.js +7 -3
- package/src/docgen/resource/view.js +130 -0
- package/src/docgen/resource-content.js +6 -8
- package/src/index.js +3 -2
- package/src/logger.js +52 -0
- package/src/providerdev/analyze.js +169 -0
- package/src/providerdev/generate.js +307 -0
- package/src/providerdev/index.js +10 -0
- package/src/providerdev/split.js +492 -0
- package/src/utils.js +42 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/providerdev/analyze.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import logger from '../logger.js';
|
|
6
|
+
import { camelToSnake } from '../utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load specification from YAML or JSON file
|
|
10
|
+
* @param {string} filepath - Path to specification file
|
|
11
|
+
* @returns {Object} - Loaded specification
|
|
12
|
+
*/
|
|
13
|
+
function loadSpec(filepath) {
|
|
14
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
15
|
+
if (filepath.endsWith('.json')) {
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
return yaml.load(content);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract main 2xx response schema reference
|
|
23
|
+
* @param {Object} responseObj - Response object
|
|
24
|
+
* @returns {string} - Schema reference name
|
|
25
|
+
*/
|
|
26
|
+
function extractMain2xxResponse(responseObj) {
|
|
27
|
+
for (const [code, response] of Object.entries(responseObj)) {
|
|
28
|
+
if (code.startsWith('2')) {
|
|
29
|
+
const content = response.content || {};
|
|
30
|
+
const appJson = content['application/json'] || {};
|
|
31
|
+
const schema = appJson.schema || {};
|
|
32
|
+
|
|
33
|
+
// Case 1: Direct $ref
|
|
34
|
+
if (schema.$ref) {
|
|
35
|
+
return schema.$ref.split('/').pop();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Case 2: Array of items
|
|
39
|
+
if (schema.type === 'array') {
|
|
40
|
+
const items = schema.items || {};
|
|
41
|
+
if (items.$ref) {
|
|
42
|
+
return items.$ref.split('/').pop();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find existing mapping in x-stackQL-resources
|
|
54
|
+
* @param {Object} spec - OpenAPI spec
|
|
55
|
+
* @param {string} pathRef - Reference to path item
|
|
56
|
+
* @returns {Object} - Mapping info (resource, method, verb)
|
|
57
|
+
*/
|
|
58
|
+
function findExistingMapping(spec, pathRef) {
|
|
59
|
+
const stackQLResources = spec.components?.['x-stackQL-resources'] || {};
|
|
60
|
+
|
|
61
|
+
for (const [resourceName, resource] of Object.entries(stackQLResources)) {
|
|
62
|
+
// Check methods
|
|
63
|
+
for (const [methodName, method] of Object.entries(resource.methods || {})) {
|
|
64
|
+
if (method.operation?.$ref === pathRef) {
|
|
65
|
+
// Find SQL verb for this method
|
|
66
|
+
let sqlVerb = 'exec'; // Default if no explicit mapping
|
|
67
|
+
|
|
68
|
+
for (const [verb, methods] of Object.entries(resource.sqlVerbs || {})) {
|
|
69
|
+
for (const methodRef of methods) {
|
|
70
|
+
if (methodRef.$ref === `#/components/x-stackQL-resources/${resourceName}/methods/${methodName}`) {
|
|
71
|
+
sqlVerb = verb;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
resourceName,
|
|
79
|
+
methodName,
|
|
80
|
+
sqlVerb
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
resourceName: '',
|
|
88
|
+
methodName: '',
|
|
89
|
+
sqlVerb: ''
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analyze OpenAPI specs and generate mapping CSV
|
|
95
|
+
* @param {Object} options - Options for analysis
|
|
96
|
+
* @returns {Promise<boolean>} - Success status
|
|
97
|
+
*/
|
|
98
|
+
export async function analyze(options) {
|
|
99
|
+
const {
|
|
100
|
+
inputDir,
|
|
101
|
+
outputDir
|
|
102
|
+
} = options;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
106
|
+
const outputPath = path.join(outputDir, 'all_services.csv');
|
|
107
|
+
|
|
108
|
+
const writer = fs.createWriteStream(outputPath, { encoding: 'utf8' });
|
|
109
|
+
|
|
110
|
+
// Write header
|
|
111
|
+
writer.write('filename,path,operationId,formatted_op_id,verb,response_object,tags,formatted_tags,stackql_resource_name,stackql_method_name,stackql_verb\n');
|
|
112
|
+
|
|
113
|
+
const files = fs.readdirSync(inputDir);
|
|
114
|
+
|
|
115
|
+
for (const filename of files) {
|
|
116
|
+
if (!filename.endsWith('.yaml') && !filename.endsWith('.yml') && !filename.endsWith('.json')) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const filepath = path.join(inputDir, filename);
|
|
121
|
+
const spec = loadSpec(filepath);
|
|
122
|
+
|
|
123
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
|
|
124
|
+
for (const [verb, operation] of Object.entries(pathItem)) {
|
|
125
|
+
if (typeof operation !== 'object' || operation === null) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const operationId = operation.operationId || '';
|
|
130
|
+
// Format operationId as snake_case
|
|
131
|
+
const formattedOpId = operationId ? camelToSnake(operationId) : '';
|
|
132
|
+
|
|
133
|
+
const responseObj = operation.responses || {};
|
|
134
|
+
const responseRef = extractMain2xxResponse(responseObj);
|
|
135
|
+
const tagsList = operation.tags || [];
|
|
136
|
+
const tagsStr = tagsList.join('|');
|
|
137
|
+
|
|
138
|
+
// Format tags as snake_case
|
|
139
|
+
const formattedTags = tagsList.map(tag => camelToSnake(tag)).join('|');
|
|
140
|
+
|
|
141
|
+
// Construct the path reference as it would appear in x-stackQL-resources
|
|
142
|
+
const encodedPath = pathKey.replace(/\//g, '~1');
|
|
143
|
+
const pathRef = `#/paths/${encodedPath}/${verb}`;
|
|
144
|
+
|
|
145
|
+
// Find existing mapping if available
|
|
146
|
+
const { resourceName, methodName, sqlVerb } = findExistingMapping(spec, pathRef);
|
|
147
|
+
|
|
148
|
+
// Escape commas in fields
|
|
149
|
+
const escapedPath = pathKey.includes(',') ? `"${pathKey}"` : pathKey;
|
|
150
|
+
const escapedOperationId = operationId.includes(',') ? `"${operationId}"` : operationId;
|
|
151
|
+
const escapedFormattedOpId = formattedOpId.includes(',') ? `"${formattedOpId}"` : formattedOpId;
|
|
152
|
+
const escapedTags = tagsStr.includes(',') ? `"${tagsStr}"` : tagsStr;
|
|
153
|
+
const escapedFormattedTags = formattedTags.includes(',') ? `"${formattedTags}"` : formattedTags;
|
|
154
|
+
|
|
155
|
+
// Write row
|
|
156
|
+
writer.write(`${filename},${escapedPath},${escapedOperationId},${escapedFormattedOpId},${verb},${responseRef},${escapedTags},${escapedFormattedTags},${resourceName},${methodName},${sqlVerb}\n`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
writer.end();
|
|
162
|
+
|
|
163
|
+
logger.info(`✅ Analysis complete. Output written to: ${outputPath}`);
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
logger.error(`Failed to analyze OpenAPI specs: ${error.message}`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// src/providerdev/generate.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import csv from 'csv-parser';
|
|
6
|
+
import logger from '../logger.js';
|
|
7
|
+
import { createReadStream } from 'fs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load manifest from CSV file
|
|
11
|
+
* @param {string} configPath - Path to CSV config file
|
|
12
|
+
* @returns {Promise<Object>} - Manifest object
|
|
13
|
+
*/
|
|
14
|
+
async function loadManifest(configPath) {
|
|
15
|
+
const manifest = {};
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
createReadStream(configPath)
|
|
19
|
+
.pipe(csv())
|
|
20
|
+
.on('data', (row) => {
|
|
21
|
+
const key = `${row.filename}::${row.operationId}`;
|
|
22
|
+
manifest[key] = row;
|
|
23
|
+
})
|
|
24
|
+
.on('end', () => {
|
|
25
|
+
resolve(manifest);
|
|
26
|
+
})
|
|
27
|
+
.on('error', (error) => {
|
|
28
|
+
reject(error);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load specification from YAML or JSON file
|
|
35
|
+
* @param {string} filepath - Path to specification file
|
|
36
|
+
* @returns {Object} - Loaded specification
|
|
37
|
+
*/
|
|
38
|
+
function loadSpec(filepath) {
|
|
39
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
40
|
+
if (filepath.endsWith('.json')) {
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
return yaml.load(content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write specification to file
|
|
48
|
+
* @param {string} filepath - Output file path
|
|
49
|
+
* @param {Object} data - Data to write
|
|
50
|
+
*/
|
|
51
|
+
function writeSpec(filepath, data) {
|
|
52
|
+
fs.writeFileSync(filepath, yaml.dump(data, { sortKeys: false }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Encode reference path
|
|
57
|
+
* @param {string} path - HTTP path
|
|
58
|
+
* @param {string} verb - HTTP verb
|
|
59
|
+
* @returns {string} - Encoded reference path
|
|
60
|
+
*/
|
|
61
|
+
function encodeRefPath(path, verb) {
|
|
62
|
+
const encodedPath = path.replace(/\//g, '~1');
|
|
63
|
+
return `#/paths/${encodedPath}/${verb}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get success response information
|
|
68
|
+
* @param {Object} operation - Operation object
|
|
69
|
+
* @returns {Object} - Response information
|
|
70
|
+
*/
|
|
71
|
+
function getSuccessResponseInfo(operation) {
|
|
72
|
+
const responses = operation.responses || {};
|
|
73
|
+
const twoXxCodes = Object.keys(responses)
|
|
74
|
+
.filter(code => code.startsWith('2'))
|
|
75
|
+
.sort();
|
|
76
|
+
|
|
77
|
+
if (twoXxCodes.length === 0) {
|
|
78
|
+
return { mediaType: '', openAPIDocKey: '' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const lowest2xx = twoXxCodes[0];
|
|
82
|
+
const content = responses[lowest2xx]?.content || {};
|
|
83
|
+
const mediaTypes = Object.keys(content);
|
|
84
|
+
|
|
85
|
+
const mediaType = mediaTypes.length > 0 ? mediaTypes[0] : '';
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
mediaType,
|
|
89
|
+
openAPIDocKey: lowest2xx
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Convert string to snake_case
|
|
95
|
+
* @param {string} name - String to convert
|
|
96
|
+
* @returns {string} - Converted string
|
|
97
|
+
*/
|
|
98
|
+
function snakeCase(name) {
|
|
99
|
+
return name.replace(/-/g, '_');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate StackQL provider extensions
|
|
104
|
+
* @param {Object} options - Options for generation
|
|
105
|
+
* @returns {Promise<boolean>} - Success status
|
|
106
|
+
*/
|
|
107
|
+
export async function generate(options) {
|
|
108
|
+
const {
|
|
109
|
+
inputDir,
|
|
110
|
+
outputDir,
|
|
111
|
+
configPath,
|
|
112
|
+
providerId,
|
|
113
|
+
servers = null,
|
|
114
|
+
providerConfig = null,
|
|
115
|
+
skipFiles = []
|
|
116
|
+
} = options;
|
|
117
|
+
|
|
118
|
+
const version = 'v00.00.00000';
|
|
119
|
+
const servicesPath = path.join(outputDir, version, 'services');
|
|
120
|
+
|
|
121
|
+
// Create directories
|
|
122
|
+
fs.mkdirSync(servicesPath, { recursive: true });
|
|
123
|
+
|
|
124
|
+
// Clean all files in services output dir
|
|
125
|
+
try {
|
|
126
|
+
const files = fs.readdirSync(servicesPath);
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const filePath = path.join(servicesPath, file);
|
|
129
|
+
if (fs.statSync(filePath).isFile()) {
|
|
130
|
+
fs.unlinkSync(filePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
logger.info(`🧹 Cleared all files in ${servicesPath}`);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error(`Failed to clear files in ${servicesPath}: ${error.message}`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Delete provider.yaml file
|
|
140
|
+
const providerManifestFile = path.join(outputDir, version, 'provider.yaml');
|
|
141
|
+
if (fs.existsSync(providerManifestFile)) {
|
|
142
|
+
fs.unlinkSync(providerManifestFile);
|
|
143
|
+
logger.info(`🧹 Deleted ${providerManifestFile}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Load manifest
|
|
147
|
+
let manifest;
|
|
148
|
+
try {
|
|
149
|
+
manifest = await loadManifest(configPath);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
logger.error(`Failed to load manifest: ${error.message}`);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const providerServices = {};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const files = fs.readdirSync(inputDir);
|
|
159
|
+
|
|
160
|
+
for (const filename of files) {
|
|
161
|
+
if (skipFiles.includes(filename)) {
|
|
162
|
+
logger.info(`⭐️ Skipping ${filename} (matched --skip)`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!filename.endsWith('.yaml') && !filename.endsWith('.yml') && !filename.endsWith('.json')) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const baseName = path.basename(filename, path.extname(filename));
|
|
171
|
+
const serviceName = snakeCase(baseName);
|
|
172
|
+
|
|
173
|
+
console.log(`processing service: ${serviceName}`);
|
|
174
|
+
|
|
175
|
+
const specPath = path.join(inputDir, filename);
|
|
176
|
+
const spec = loadSpec(specPath);
|
|
177
|
+
|
|
178
|
+
// Initialize resources object with defaultdict-like behavior
|
|
179
|
+
const resources = {};
|
|
180
|
+
|
|
181
|
+
for (const [pathKey, pathItem] of Object.entries(spec.paths || {})) {
|
|
182
|
+
for (const [verb, operation] of Object.entries(pathItem)) {
|
|
183
|
+
if (typeof operation !== 'object' || operation === null) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const operationId = operation.operationId;
|
|
188
|
+
if (!operationId) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const manifestKey = `${filename}::${operationId}`;
|
|
193
|
+
const entry = manifest[manifestKey];
|
|
194
|
+
if (!entry) {
|
|
195
|
+
logger.error(`❌ ERROR: ${filename} → ${operationId} not found in manifest`);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const resource = entry.stackql_resource_name;
|
|
200
|
+
const method = entry.stackql_method_name;
|
|
201
|
+
const sqlverb = entry.stackql_verb;
|
|
202
|
+
|
|
203
|
+
// Initialize resource if it doesn't exist
|
|
204
|
+
if (!resources[resource]) {
|
|
205
|
+
resources[resource] = {
|
|
206
|
+
id: `${providerId}.${serviceName}.${resource}`,
|
|
207
|
+
name: resource,
|
|
208
|
+
title: resource.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
209
|
+
methods: {},
|
|
210
|
+
sqlVerbs: {
|
|
211
|
+
select: [],
|
|
212
|
+
insert: [],
|
|
213
|
+
update: [],
|
|
214
|
+
delete: [],
|
|
215
|
+
replace: []
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const pathRef = encodeRefPath(pathKey, verb);
|
|
221
|
+
const responseInfo = getSuccessResponseInfo(operation);
|
|
222
|
+
|
|
223
|
+
const methodEntry = {
|
|
224
|
+
operation: { $ref: pathRef },
|
|
225
|
+
response: responseInfo
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
resources[resource].methods[method] = methodEntry;
|
|
229
|
+
if (sqlverb && sqlverb === 'exec') {
|
|
230
|
+
logger.info(`exec method skipped: ${resource}.${method}`);
|
|
231
|
+
} else if (sqlverb && resources[resource].sqlVerbs[sqlverb]) {
|
|
232
|
+
resources[resource].sqlVerbs[sqlverb].push({
|
|
233
|
+
$ref: `#/components/x-stackQL-resources/${resource}/methods/${method}`
|
|
234
|
+
});
|
|
235
|
+
} else if (sqlverb) {
|
|
236
|
+
logger.warn(`⚠️ Unknown SQL verb '${sqlverb}' for ${resource}.${method}, skipping`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Inject into spec
|
|
242
|
+
if (!spec.components) {
|
|
243
|
+
spec.components = {};
|
|
244
|
+
}
|
|
245
|
+
spec.components['x-stackQL-resources'] = resources;
|
|
246
|
+
|
|
247
|
+
// Inject servers if provided
|
|
248
|
+
if (servers) {
|
|
249
|
+
try {
|
|
250
|
+
const serversJson = JSON.parse(servers);
|
|
251
|
+
spec.servers = serversJson;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
logger.error(`❌ Failed to parse servers JSON: ${error.message}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Write enriched spec
|
|
259
|
+
const outputPath = path.join(servicesPath, filename);
|
|
260
|
+
writeSpec(outputPath, spec);
|
|
261
|
+
logger.info(`✅ Wrote enriched spec: ${outputPath}`);
|
|
262
|
+
|
|
263
|
+
// Add providerService entry
|
|
264
|
+
const info = spec.info || {};
|
|
265
|
+
const specTitle = info.title || `${serviceName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())} API`;
|
|
266
|
+
const specDescription = info.description || `TODO: add description for ${serviceName}`;
|
|
267
|
+
|
|
268
|
+
providerServices[serviceName] = {
|
|
269
|
+
id: `${serviceName}:${version}`,
|
|
270
|
+
name: serviceName,
|
|
271
|
+
preferred: true,
|
|
272
|
+
service: {
|
|
273
|
+
$ref: `${providerId}/${version}/services/${filename}`
|
|
274
|
+
},
|
|
275
|
+
title: specTitle,
|
|
276
|
+
version: version,
|
|
277
|
+
description: specDescription
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Write provider.yaml
|
|
282
|
+
const providerYaml = {
|
|
283
|
+
id: providerId,
|
|
284
|
+
name: providerId,
|
|
285
|
+
version: version,
|
|
286
|
+
providerServices: providerServices,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (providerConfig) {
|
|
290
|
+
try {
|
|
291
|
+
const providerConfigJson = JSON.parse(providerConfig);
|
|
292
|
+
providerYaml.config = providerConfigJson;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logger.error(`❌ Failed to parse provider config JSON: ${error.message}`);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
writeSpec(path.join(outputDir, version, 'provider.yaml'), providerYaml);
|
|
300
|
+
logger.info(`📦 Wrote provider.yaml to ${outputDir}/${version}/provider.yaml`);
|
|
301
|
+
|
|
302
|
+
return true;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
logger.error(`Failed to generate provider: ${error.message}`);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|