@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,492 @@
|
|
|
1
|
+
// @stackql/provider-utils/src/providerdev/split.js
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import logger from '../logger.js';
|
|
7
|
+
import {
|
|
8
|
+
camelToSnake,
|
|
9
|
+
createDestDir
|
|
10
|
+
} from '../utils.js';
|
|
11
|
+
|
|
12
|
+
// Constants
|
|
13
|
+
const OPERATIONS = ["get", "post", "put", "delete", "patch", "options", "head", "trace"];
|
|
14
|
+
const NON_OPERATIONS = ["parameters", "servers", "summary", "description"];
|
|
15
|
+
const COMPONENTS_CHILDREN = ["schemas", "responses", "parameters", "examples", "requestBodies", "headers", "securitySchemes", "links", "callbacks"];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if operation should be excluded
|
|
19
|
+
* @param {string[]} exclude - List of exclusion criteria
|
|
20
|
+
* @param {Object} opItem - Operation item from OpenAPI doc
|
|
21
|
+
* @param {string} svcDiscriminator - Service discriminator
|
|
22
|
+
* @returns {boolean} - Whether operation should be excluded
|
|
23
|
+
*/
|
|
24
|
+
function isOperationExcluded(exclude, opItem) {
|
|
25
|
+
if (!exclude || exclude.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Example: exclude based on tags or other criteria
|
|
30
|
+
if (opItem.tags && opItem.tags.some(tag => exclude.includes(tag))) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determine service name and description using discriminator
|
|
39
|
+
* @param {string} providerName - Provider name
|
|
40
|
+
* @param {Object} opItem - Operation item
|
|
41
|
+
* @param {string} pathKey - Path key
|
|
42
|
+
* @param {string} svcDiscriminator - Service discriminator
|
|
43
|
+
* @param {Object[]} allTags - All tags from API doc
|
|
44
|
+
* @param {boolean} debug - Debug flag
|
|
45
|
+
* @returns {[string, string]} - [service name, service description]
|
|
46
|
+
*/
|
|
47
|
+
function retServiceNameAndDesc(providerName, opItem, pathKey, svcDiscriminator, allTags, debug) {
|
|
48
|
+
let service = "default";
|
|
49
|
+
let serviceDesc = `${providerName} API`;
|
|
50
|
+
|
|
51
|
+
// Use tags if discriminator is "tag"
|
|
52
|
+
if (svcDiscriminator === "tag" && opItem.tags && opItem.tags.length > 0) {
|
|
53
|
+
service = opItem.tags[0].toLowerCase().replace(/-/g, '_').replace(/ /g, '_');
|
|
54
|
+
|
|
55
|
+
// Find description in all_tags
|
|
56
|
+
for (const tag of allTags) {
|
|
57
|
+
if (tag.name === service) {
|
|
58
|
+
serviceDesc = tag.description || serviceDesc;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Use first significant path segment if discriminator is "path"
|
|
65
|
+
else if (svcDiscriminator === "path") {
|
|
66
|
+
const pathParts = pathKey.replace(/^\//, '').split('/');
|
|
67
|
+
if (pathParts.length > 0) {
|
|
68
|
+
// Find the first path segment that is not 'api' or 'v{number}'
|
|
69
|
+
for (const part of pathParts) {
|
|
70
|
+
const lowerPart = part.toLowerCase();
|
|
71
|
+
// Skip if it's 'api' or matches version pattern 'v1', 'v2', etc.
|
|
72
|
+
if (lowerPart === 'api' || /^v\d+$/.test(lowerPart)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
service = lowerPart.replace(/-/g, '_').replace(/ /g, '_').replace(/\./g, '_');
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
serviceDesc = `${providerName} ${service} API`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if service should be skipped
|
|
83
|
+
if (service === "skip") {
|
|
84
|
+
return ["skip", ""];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [service, serviceDesc];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Initialize service map
|
|
92
|
+
* @param {Object} services - Services map
|
|
93
|
+
* @param {string[]} componentsChildren - Components children
|
|
94
|
+
* @param {string} service - Service name
|
|
95
|
+
* @param {string} serviceDesc - Service description
|
|
96
|
+
* @param {Object} apiDoc - API doc
|
|
97
|
+
* @returns {Object} - Updated services map
|
|
98
|
+
*/
|
|
99
|
+
function initService(services, componentsChildren, service, serviceDesc, apiDoc) {
|
|
100
|
+
services[service] = {
|
|
101
|
+
openapi: apiDoc.openapi || "3.0.0",
|
|
102
|
+
info: {
|
|
103
|
+
title: `${service} API`,
|
|
104
|
+
description: serviceDesc,
|
|
105
|
+
version: apiDoc.info?.version || "1.0.0"
|
|
106
|
+
},
|
|
107
|
+
paths: {},
|
|
108
|
+
components: {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Initialize components sections
|
|
112
|
+
services[service].components = {};
|
|
113
|
+
for (const child of componentsChildren) {
|
|
114
|
+
services[service].components[child] = {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Copy servers if present
|
|
118
|
+
if (apiDoc.servers) {
|
|
119
|
+
services[service].servers = apiDoc.servers;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return services;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract all $ref values from an object recursively
|
|
127
|
+
* @param {any} obj - Object to extract refs from
|
|
128
|
+
* @returns {Set<string>} - Set of refs
|
|
129
|
+
*/
|
|
130
|
+
function getAllRefs(obj) {
|
|
131
|
+
const refs = new Set();
|
|
132
|
+
|
|
133
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
134
|
+
if (Array.isArray(obj)) {
|
|
135
|
+
for (const item of obj) {
|
|
136
|
+
for (const ref of getAllRefs(item)) {
|
|
137
|
+
refs.add(ref);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
142
|
+
if (key === "$ref" && typeof value === 'string') {
|
|
143
|
+
refs.add(value);
|
|
144
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
145
|
+
for (const ref of getAllRefs(value)) {
|
|
146
|
+
refs.add(ref);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return refs;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add referenced components to service
|
|
158
|
+
* @param {Set<string>} refs - Set of refs
|
|
159
|
+
* @param {Object} service - Service object
|
|
160
|
+
* @param {Object} components - Components from API doc
|
|
161
|
+
* @param {boolean} debug - Debug flag
|
|
162
|
+
*/
|
|
163
|
+
function addRefsToComponents(refs, service, components, debug) {
|
|
164
|
+
for (const ref of refs) {
|
|
165
|
+
const parts = ref.split('/');
|
|
166
|
+
|
|
167
|
+
// Only process refs that point to components
|
|
168
|
+
if (parts.length >= 4 && parts[1] === "components") {
|
|
169
|
+
const componentType = parts[2];
|
|
170
|
+
const componentName = parts[3];
|
|
171
|
+
|
|
172
|
+
// Check if component type exists in service
|
|
173
|
+
if (!service.components[componentType]) {
|
|
174
|
+
service.components[componentType] = {};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Skip if component already added
|
|
178
|
+
if (service.components[componentType][componentName]) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add component if it exists in source document
|
|
183
|
+
if (components[componentType] && components[componentType][componentName]) {
|
|
184
|
+
service.components[componentType][componentName] = components[componentType][componentName];
|
|
185
|
+
if (debug) {
|
|
186
|
+
logger.debug(`Added component ${componentType}/${componentName}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Add missing type: object to schema objects
|
|
195
|
+
* @param {any} obj - Object to process
|
|
196
|
+
* @returns {any} - Processed object
|
|
197
|
+
*/
|
|
198
|
+
function addMissingObjectTypes(obj) {
|
|
199
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
200
|
+
return obj;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(obj)) {
|
|
204
|
+
return obj.map(item => addMissingObjectTypes(item));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If it has properties but no type, add type: object
|
|
208
|
+
if (obj.properties && !obj.type) {
|
|
209
|
+
obj.type = "object";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Process nested objects
|
|
213
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
214
|
+
if (typeof value === 'object' && value !== null) {
|
|
215
|
+
obj[key] = addMissingObjectTypes(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return obj;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Split OpenAPI document into service-specific files
|
|
224
|
+
* @param {Object} options - Options for splitting
|
|
225
|
+
* @returns {Promise<boolean>} - Success status
|
|
226
|
+
*/
|
|
227
|
+
export async function split(options) {
|
|
228
|
+
const {
|
|
229
|
+
apiDoc,
|
|
230
|
+
providerName,
|
|
231
|
+
outputDir,
|
|
232
|
+
svcDiscriminator = "tag",
|
|
233
|
+
exclude = null,
|
|
234
|
+
overwrite = true,
|
|
235
|
+
verbose = false
|
|
236
|
+
} = options;
|
|
237
|
+
|
|
238
|
+
// Setup logging based on verbosity
|
|
239
|
+
if (verbose) {
|
|
240
|
+
logger.level = 'debug';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
logger.info(`📄 Splitting OpenAPI doc for ${providerName}`);
|
|
244
|
+
logger.info(`API Doc: ${apiDoc}`);
|
|
245
|
+
logger.info(`Output: ${outputDir}`);
|
|
246
|
+
logger.info(`Service Discriminator: ${svcDiscriminator}`);
|
|
247
|
+
|
|
248
|
+
// Process exclude list
|
|
249
|
+
const excludeList = exclude ? exclude.split(",") : [];
|
|
250
|
+
|
|
251
|
+
// Read the OpenAPI document
|
|
252
|
+
let apiDocObj;
|
|
253
|
+
try {
|
|
254
|
+
const apiDocContent = fs.readFileSync(apiDoc, 'utf8');
|
|
255
|
+
apiDocObj = yaml.load(apiDocContent);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
logger.error(`❌ Failed to parse ${apiDoc}: ${e.message}`);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Create destination directory
|
|
262
|
+
if (!createDestDir(outputDir, overwrite)) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get API paths
|
|
267
|
+
const apiPaths = apiDocObj.paths || {};
|
|
268
|
+
logger.info(`🔑 Iterating over ${Object.keys(apiPaths).length} paths`);
|
|
269
|
+
|
|
270
|
+
const services = {};
|
|
271
|
+
let opCounter = 0;
|
|
272
|
+
|
|
273
|
+
// Process each path
|
|
274
|
+
for (const [pathKey, pathItem] of Object.entries(apiPaths)) {
|
|
275
|
+
if (verbose) {
|
|
276
|
+
logger.debug(`Processing path ${pathKey}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!pathItem) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Process each operation (HTTP verb)
|
|
284
|
+
for (const [verbKey, opItem] of Object.entries(pathItem)) {
|
|
285
|
+
if (!OPERATIONS.includes(verbKey) || !opItem) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
opCounter += 1;
|
|
290
|
+
if (opCounter % 100 === 0) {
|
|
291
|
+
logger.info(`⚙️ Operations processed: ${opCounter}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (verbose) {
|
|
295
|
+
logger.debug(`Processing operation ${pathKey}:${verbKey}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Skip excluded operations
|
|
299
|
+
if (isOperationExcluded(excludeList, opItem, svcDiscriminator)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Determine service name
|
|
304
|
+
const [service, serviceDesc] = retServiceNameAndDesc(
|
|
305
|
+
providerName, opItem, pathKey, svcDiscriminator,
|
|
306
|
+
apiDocObj.tags || [], verbose
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Skip if service is marked to skip
|
|
310
|
+
if (service === 'skip') {
|
|
311
|
+
logger.warn(`⭐️ Skipping service: ${service}`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (verbose) {
|
|
316
|
+
logger.debug(`Service name: ${service}`);
|
|
317
|
+
logger.debug(`Service desc: ${serviceDesc}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Initialize service if first occurrence
|
|
321
|
+
if (!services[service]) {
|
|
322
|
+
if (verbose) {
|
|
323
|
+
logger.debug(`First occurrence of ${service}`);
|
|
324
|
+
}
|
|
325
|
+
initService(services, COMPONENTS_CHILDREN, service, serviceDesc, apiDocObj);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Add operation to service
|
|
329
|
+
if (!services[service].paths[pathKey]) {
|
|
330
|
+
if (verbose) {
|
|
331
|
+
logger.debug(`First occurrence of ${pathKey}`);
|
|
332
|
+
}
|
|
333
|
+
services[service].paths[pathKey] = {};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
services[service].paths[pathKey][verbKey] = opItem;
|
|
337
|
+
|
|
338
|
+
// Special case for GitHub
|
|
339
|
+
if (providerName === 'github' &&
|
|
340
|
+
opItem['x-github'] &&
|
|
341
|
+
opItem['x-github'].subcategory) {
|
|
342
|
+
services[service].paths[pathKey][verbKey]['x-stackQL-resource'] = camelToSnake(
|
|
343
|
+
opItem['x-github'].subcategory
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Get all refs for operation
|
|
348
|
+
const opRefs = getAllRefs(opItem);
|
|
349
|
+
|
|
350
|
+
if (verbose) {
|
|
351
|
+
logger.debug(`Found ${opRefs.size} refs for ${service}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Add refs to components
|
|
355
|
+
addRefsToComponents(opRefs, services[service], apiDocObj.components || {}, verbose);
|
|
356
|
+
|
|
357
|
+
// Get internal refs
|
|
358
|
+
for (let i = 0; i < 3; i++) { // Internal ref depth
|
|
359
|
+
const intRefs = getAllRefs(services[service].components);
|
|
360
|
+
if (verbose) {
|
|
361
|
+
logger.debug(`Found ${intRefs.size} INTERNAL refs for service ${service}`);
|
|
362
|
+
}
|
|
363
|
+
addRefsToComponents(intRefs, services[service], apiDocObj.components || {}, verbose);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Get deeply nested schema refs
|
|
367
|
+
for (let i = 0; i < 10; i++) { // Schema max ref depth
|
|
368
|
+
const intRefs = getAllRefs(services[service].components);
|
|
369
|
+
// Filter refs that are already in service components
|
|
370
|
+
const filteredRefs = new Set();
|
|
371
|
+
for (const ref of intRefs) {
|
|
372
|
+
const parts = ref.split('/');
|
|
373
|
+
if (parts.length >= 4 && parts[1] === "components" && parts[2] === "schemas" &&
|
|
374
|
+
!services[service].components.schemas[parts[3]]) {
|
|
375
|
+
filteredRefs.add(ref);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (verbose) {
|
|
380
|
+
logger.debug(`Found ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (filteredRefs.size > 0) {
|
|
384
|
+
if (verbose) {
|
|
385
|
+
logger.debug(`Adding ${filteredRefs.size} INTERNAL schema refs for service ${service}`);
|
|
386
|
+
}
|
|
387
|
+
addRefsToComponents(filteredRefs, services[service], apiDocObj.components || {}, verbose);
|
|
388
|
+
} else {
|
|
389
|
+
if (verbose) {
|
|
390
|
+
logger.debug(`Exiting INTERNAL schema refs for ${service}`);
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Add non-operations to each service
|
|
399
|
+
for (const service in services) {
|
|
400
|
+
for (const pathKey of Object.keys(services[service].paths)) {
|
|
401
|
+
if (verbose) {
|
|
402
|
+
logger.debug(`Adding non operations to ${service} for path ${pathKey}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const nonOp of NON_OPERATIONS) {
|
|
406
|
+
if (verbose) {
|
|
407
|
+
logger.debug(`Looking for non operation ${nonOp} in ${service} under path ${pathKey}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (apiPaths[pathKey] && apiPaths[pathKey][nonOp]) {
|
|
411
|
+
if (verbose) {
|
|
412
|
+
logger.debug(`Adding ${nonOp} to ${service} for path ${pathKey}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Special case for parameters
|
|
416
|
+
if (nonOp === 'parameters') {
|
|
417
|
+
for (const verbKey in services[service].paths[pathKey]) {
|
|
418
|
+
services[service].paths[pathKey][verbKey].parameters = apiPaths[pathKey].parameters;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Update path param names (replace hyphens with underscores)
|
|
427
|
+
for (const service in services) {
|
|
428
|
+
if (services[service].paths) {
|
|
429
|
+
const pathKeys = Object.keys(services[service].paths);
|
|
430
|
+
for (const pathKey of pathKeys) {
|
|
431
|
+
if (verbose) {
|
|
432
|
+
logger.debug(`Renaming path params in ${service} for path ${pathKey}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Replace hyphens with underscores in path parameters
|
|
436
|
+
const updatedPathKey = pathKey.replace(/(?<=\{)([^}]+?)-([^}]+?)(?=\})/g, '$1_$2');
|
|
437
|
+
|
|
438
|
+
if (updatedPathKey !== pathKey) {
|
|
439
|
+
if (verbose) {
|
|
440
|
+
logger.debug(`Updated path key from ${pathKey} to ${updatedPathKey}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
services[service].paths[updatedPathKey] = services[service].paths[pathKey];
|
|
444
|
+
delete services[service].paths[pathKey];
|
|
445
|
+
|
|
446
|
+
// Also update parameter names in operations
|
|
447
|
+
for (const verbKey in services[service].paths[updatedPathKey]) {
|
|
448
|
+
const operation = services[service].paths[updatedPathKey][verbKey];
|
|
449
|
+
if (operation.parameters) {
|
|
450
|
+
for (const param of operation.parameters) {
|
|
451
|
+
if (param.in === 'path' && param.name.includes('-')) {
|
|
452
|
+
const originalName = param.name;
|
|
453
|
+
param.name = param.name.replace(/-/g, '_');
|
|
454
|
+
if (verbose) {
|
|
455
|
+
logger.debug(`Updated parameter name from ${originalName} to ${param.name} in path ${updatedPathKey}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fix missing type: object
|
|
467
|
+
for (const service in services) {
|
|
468
|
+
if (verbose) {
|
|
469
|
+
logger.debug(`Updating paths for ${service}`);
|
|
470
|
+
}
|
|
471
|
+
services[service].paths = addMissingObjectTypes(services[service].paths);
|
|
472
|
+
services[service].components = addMissingObjectTypes(services[service].components);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Write out service docs
|
|
476
|
+
for (const service in services) {
|
|
477
|
+
logger.info(`✅ Writing out OpenAPI doc for [${service}]`);
|
|
478
|
+
|
|
479
|
+
// const svcDir = path.join(outputDir, service);
|
|
480
|
+
// const outputFile = path.join(svcDir, `${service}.yaml`);
|
|
481
|
+
const outputFile = path.join(outputDir, `${service}.yaml`);
|
|
482
|
+
// fs.mkdirSync(svcDir, { recursive: true });
|
|
483
|
+
|
|
484
|
+
fs.writeFileSync(outputFile, yaml.dump(services[service], {
|
|
485
|
+
noRefs: true,
|
|
486
|
+
sortKeys: false
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
logger.info(`🎉 Successfully split OpenAPI doc into ${Object.keys(services).length} services`);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Add to utils.js at the top:
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import logger from './logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert camelCase to snake_case
|
|
7
|
+
* @param {string} name - The string to convert
|
|
8
|
+
* @returns {string} - Converted string in snake_case
|
|
9
|
+
*/
|
|
10
|
+
export function camelToSnake(name) {
|
|
11
|
+
const s1 = name.replace(/([a-z0-9])([A-Z][a-z]+)/g, '$1_$2');
|
|
12
|
+
return s1.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create destination directory
|
|
17
|
+
* @param {string} destDir - Destination directory path
|
|
18
|
+
* @param {boolean} overwrite - Whether to overwrite existing directory
|
|
19
|
+
* @returns {boolean} - Success status
|
|
20
|
+
*/
|
|
21
|
+
export function createDestDir(destDir, overwrite) {
|
|
22
|
+
if (fs.existsSync(destDir)) {
|
|
23
|
+
if (!overwrite) {
|
|
24
|
+
logger.error(`Destination directory ${destDir} already exists. Use --overwrite to force.`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Clean the directory if overwrite is true
|
|
29
|
+
try {
|
|
30
|
+
// Remove all files and subdirectories
|
|
31
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
32
|
+
logger.info(`Cleaned destination directory ${destDir}`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error(`Failed to clean destination directory ${destDir}: ${error.message}`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create the directory (or recreate it if it was deleted)
|
|
40
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
41
|
+
return true;
|
|
42
|
+
}
|