UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

335 lines (334 loc) 15.1 kB
import { ProjectAssetValidator } from './validator/asset-validator.js'; import JSZip from 'jszip'; import yaml from 'js-yaml'; import { convertNumberToString, isValidAsset, updateMapWithMetadata, updateRefs } from './utils.js'; import path from 'path'; export class BuildProjectAssets { async loadZipFromBuffer(fileBuffer) { console.log('Loading ZIP from buffer'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } async validate(fileBuffer) { console.log('Validating ZIP file'); const AssetValidator = new ProjectAssetValidator(); const folderNames = new Set(); const filePathsInFolder = new Set(); await this.extractFolderNamesAndPaths(fileBuffer, folderNames, filePathsInFolder); const validationPromises = Array.from(folderNames).map(async (folderName) => { return this.validateFolder(fileBuffer, folderName, AssetValidator, filePathsInFolder, folderNames); }); const validationResults = await Promise.all(validationPromises); const isValid = validationResults.every(result => result.isValid); // create a map with folderName and it's refMap const allRefMaps = new Map(); validationResults.forEach((result, index) => { const folderName = Array.from(folderNames)[index]; allRefMaps.set(folderName, result.refMap); }); return { isValid, allRefMaps }; } async extractFolderNamesAndPaths(fileBuffer, folderNames, filePathsInFolder) { console.log('Extracting folder names and paths'); const zipContent = await this.loadZipFromBuffer(fileBuffer); for (const fileName in zipContent.files) { const folderName = fileName.split(path.sep)[0]; if (folderName !== 'dependencies') { folderNames.add(folderName); filePathsInFolder.add(fileName); } } console.log(Array.from(folderNames).join(', ')); } async validateFolder(fileBuffer, folderName, AssetValidator, filePathsInFolder, allFolderNames) { console.log(`folder: ${folderName}`); const assetReferenceValid = await AssetValidator.validateProjectAssetReference(fileBuffer, folderName, allFolderNames); const pathReferenceValid = await AssetValidator.validateProjectPathReference(fileBuffer, folderName, filePathsInFolder); const minimumAssetsValid = await AssetValidator.validateProjectHasMinimumAssets(fileBuffer); const apiSpecVariableValid = await AssetValidator.validateProjectApiSpecVariable(fileBuffer, folderName); const yamlValid = await AssetValidator.validateYaml(fileBuffer); // return isvalid true if all the validations are true // return refMap to consolidate assets later return { isValid: assetReferenceValid.isValid && pathReferenceValid && minimumAssetsValid && apiSpecVariableValid && yamlValid, refMap: assetReferenceValid.refMap }; } async getFileFromZip(fileBuffer, filePath) { console.log(filePath); const zipContent = await this.loadZipFromBuffer(fileBuffer); const file = zipContent.file(filePath); if (file) { return file.async('string'); } console.log(filePath); return null; } async createProjectBuildZip(buffer, allRefMaps) { console.log('Creating project build ZIP'); const buildZip = new JSZip(); await this.loadZipFromBuffer(buffer); const folderNames = new Set(); const filePathsInFolder = new Set(); await this.extractFolderNamesAndPaths(buffer, folderNames, filePathsInFolder); await this.addConsolidatedYAMLs(buildZip, buffer, folderNames, allRefMaps); await this.addReferencedFiles(buildZip, buffer, folderNames); return buildZip; } async addConsolidatedYAMLs(buildZip, buffer, folderNames, allRefMaps) { console.log('Adding consolidated YAMLs to build ZIP'); for (const folderName of folderNames) { const consolidatedYaml = await this.createConsolidatedYaml(buffer, folderName, allRefMaps); buildZip.file(`${folderName}.yaml`, consolidatedYaml); } } async addReferencedFiles(buildZip, buffer, folderNames) { console.log('Adding referenced files to build ZIP'); const AssetValidator = new ProjectAssetValidator(); for (const folderName of folderNames) { const refMap = await AssetValidator.createProjectPathReferenceMap(buffer, folderName); const promises = Array.from(refMap.keys()).map(async (key) => { const file = await this.getFileFromZip(buffer, path.normalize(`${folderName}/${key}`)); if (file !== null) { buildZip.file(path.normalize(`resources/${folderName}/${key}`), file); } else { console.log(key); } }); await Promise.all(promises); } } async createConsolidatedYaml(buffer, folderName, allRefMaps) { console.log(folderName); const zipContent = await this.loadZipFromBuffer(buffer); const visitedAsset = new Set(); let consolidatedYaml = ''; try { const versionMap = await this.createVersionProcessingMap(buffer); consolidatedYaml += await this.processYamlFiles(zipContent, folderName, visitedAsset, versionMap); consolidatedYaml += await this.processDependencyFiles(zipContent, visitedAsset, versionMap); consolidatedYaml += await this.processDependenciesInOtherFolders(zipContent, visitedAsset, versionMap, folderName, allRefMaps); } catch (err) { console.log('processing ZIP', `${err}`); } return consolidatedYaml; } async processYamlFiles(zipContent, folderName, visitedAsset, versionMap) { let consolidatedYaml = ''; for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (!entry.dir && fileName.startsWith(folderName + path.sep) && (fileName.endsWith('.yaml') || fileName.endsWith('.yml'))) { try { const content = await entry.async('string'); consolidatedYaml += await this.processYamlContent(content, visitedAsset, versionMap); } catch (err) { console.log('parsing YAML file', `${err}`); } } } return consolidatedYaml; } async processDependenciesInOtherFolders(zipContent, visitedAsset, versionMap, folderName, allRefMaps) { console.log('Processing dependency YAML files'); let consolidatedYaml = ''; // retrieve the refMap of the current folderName and process it for (const [refFolderName, refMap] of allRefMaps) { if (refFolderName === folderName) { for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.shouldProcessFilesInOtherFolders(entry, fileName, folderName)) { try { const content = await entry.async('string'); consolidatedYaml += await this.processYamlContentForOtherFolders(content, visitedAsset, versionMap, refMap); } catch (err) { console.log('parsing dependency YAML file', `${err}`); } } } } } return consolidatedYaml; } async processDependencyFiles(zipContent, visitedAsset, versionMap) { console.log('Processing dependency YAML files'); let consolidatedYaml = ''; for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (!entry.dir && fileName.startsWith('dependencies' + path.sep) && (fileName.endsWith('.yaml') || fileName.endsWith('.yml'))) { try { const content = await entry.async('string'); consolidatedYaml += await this.processYamlContent(content, visitedAsset, versionMap); } catch (err) { console.log('parsing dependency YAML file', `${err}`); } } } return consolidatedYaml; } shouldProcessFile(fileName) { return (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) && !fileName.includes('resources'); } shouldProcessFilesInOtherFolders(entry, fileName, folderName) { return !entry.dir && !fileName.startsWith(folderName + path.sep) && (fileName.endsWith('.yaml') || fileName.endsWith('.yml')); } async createVersionProcessingMap(buffer) { const refMap = new Map(); try { const zipContent = await this.loadZipFromBuffer(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.shouldProcessFile(fileName) && !entry.dir) { await this.processFileContent(entry, refMap); } } } catch (err) { console.log(`Error loading ZIP: ${err}`); } return refMap; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async processFileContent(entry, refMap) { try { const content = await entry.async('string'); const yamlContents = this.parseYaml(content); this.processYamlContents(yamlContents, refMap); } catch (err) { console.log(`Error parsing YAML: ${err}`); } } parseYaml(content) { try { return yaml.loadAll(content); } catch (err) { console.log(`Error parsing YAML: ${err}`); return []; } } processYamlContents(yamlContents, refMap) { for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent['metadata']; if (typeof metadata.version === 'string') { updateMapWithMetadata(yamlContent, refMap); } } } } async processYamlContent(content, visitedAsset, versionMap) { let yamlResult = ''; try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent.metadata; const contentString = `${metadata.namespace ? metadata.namespace : ''}:${metadata.name}:${convertNumberToString(metadata.version)}`; if (visitedAsset.has(contentString)) { console.log(`Skipping already visited asset: ${contentString}`); continue; } yamlContent.metadata.version = convertNumberToString(metadata.version); visitedAsset.add(contentString); const processedYamlContent = updateRefs(yamlContent, versionMap); yamlResult += '---' + '\n' + yaml.dump(processedYamlContent) + '\n'; } } } catch (err) { console.log('processing YAML content', `${err}`); } return yamlResult; } async processYamlContentForOtherFolders(content, visitedAsset, versionMap, refMap) { let yamlResult = ''; try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent.metadata; const contentString = `${metadata.namespace ? metadata.namespace : ''}:${metadata.name}:${convertNumberToString(metadata.version)}`; // If the current file metadata is present in refMap and the metadata value is set to true, then it needs to be added if (refMap.has(contentString) && refMap.get(contentString) === true) { if (visitedAsset.has(contentString)) { console.log(`Skipping already visited asset: ${contentString}`); continue; } yamlContent.metadata.version = convertNumberToString(metadata.version); visitedAsset.add(contentString); const processedYamlContent = updateRefs(yamlContent, versionMap); yamlResult += '---' + '\n' + yaml.dump(processedYamlContent) + '\n'; } } } } catch (err) { console.log('processing YAML content', `${err}`); } return yamlResult; } async processProjectZip(fileBuffer) { console.log('Processing project ZIP'); const validatedFileBuffer = await this.validate(fileBuffer); if (!validatedFileBuffer.isValid) { console.log('Project ZIP validation failed'); return null; } return this.createProjectBuildZip(fileBuffer, validatedFileBuffer.allRefMaps); } async extractGatewaysJson(buffer) { console.log('Extracting gateways.json'); const zipContent = await this.loadZipFromBuffer(buffer); return this.extractGatewaysJsonFromZip(zipContent); } async extractGatewaysJsonFromZip(zipContent) { console.log('Extracting gateways.json from ZIP'); let gatewaysJsonContent = {}; try { const gatewaysJsonFile = this.findGatewaysJsonFile(zipContent); if (gatewaysJsonFile) { gatewaysJsonContent = await this.parseJsonContent(gatewaysJsonFile); } else { console.log('gateways.json file not found in ZIP'); } } catch (err) { console.log('extracting gateways.json', `${err}`); } return gatewaysJsonContent; } findGatewaysJsonFile(zipContent) { console.log('Finding gateways.json file in ZIP'); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (!entry.dir && fileName.includes('gateways.json')) { return entry; } } return null; } async parseJsonContent(file) { console.log('Parsing JSON content'); let jsonContent = {}; try { const content = await file.async('string'); jsonContent = JSON.parse(content); } catch (err) { console.log('parsing JSON content', `${err}`); } return jsonContent; } }