UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

395 lines (341 loc) 14.4 kB
/** * Copyright Super iPaaS Integration LLC, an IBM Company 2024 */ import { Metadata_Ref, YamlContent, ReferenceValidationResult, ReferenceValidationResultMap} from './model/interface.js'; import { GatewaysJson } from '@apic/studio-shared'; 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 { private async loadZipFromBuffer(fileBuffer: Buffer): Promise<JSZip> { console.log('Loading ZIP from buffer'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } private async validate(fileBuffer: Buffer): Promise<ReferenceValidationResultMap> { console.log('Validating ZIP file'); const AssetValidator = new ProjectAssetValidator(); const folderNames = new Set<string>(); const filePathsInFolder = new Set<string>(); 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<string, Map<string, boolean>>(); validationResults.forEach((result, index) => { const folderName = Array.from(folderNames)[index]; allRefMaps.set(folderName, result.refMap); }); return { isValid, allRefMaps }; } private async extractFolderNamesAndPaths(fileBuffer: Buffer, folderNames: Set<string>, filePathsInFolder: Set<string>): Promise<void> { 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(', ')); } private async validateFolder( fileBuffer: Buffer, folderName: string, AssetValidator: ProjectAssetValidator, filePathsInFolder: Set<string>, allFolderNames: Set<string> ): Promise<ReferenceValidationResult> { 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 }; } private async getFileFromZip(fileBuffer: Buffer, filePath: string): Promise<string | null> { 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; } private async createProjectBuildZip(buffer: Buffer, allRefMaps: Map<string, Map<string, boolean>>): Promise<JSZip> { console.log('Creating project build ZIP'); const buildZip = new JSZip(); await this.loadZipFromBuffer(buffer); const folderNames = new Set<string>(); const filePathsInFolder = new Set<string>(); await this.extractFolderNamesAndPaths(buffer, folderNames, filePathsInFolder); await this.addConsolidatedYAMLs(buildZip, buffer, folderNames, allRefMaps); await this.addReferencedFiles(buildZip, buffer, folderNames); return buildZip; } private async addConsolidatedYAMLs(buildZip: JSZip, buffer: Buffer, folderNames: Set<string>, allRefMaps: Map<string, Map<string, boolean>>): Promise<void> { 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); } } private async addReferencedFiles(buildZip: JSZip, buffer: Buffer, folderNames: Set<string>): Promise<void> { 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); } } private async createConsolidatedYaml(buffer: Buffer, folderName: string, allRefMaps: Map<string, Map<string, boolean>>): Promise<string> { console.log(folderName); const zipContent = await this.loadZipFromBuffer(buffer); const visitedAsset = new Set<string>(); 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; } private async processYamlFiles(zipContent: JSZip, folderName: string, visitedAsset: Set<string>,versionMap:Map<string,boolean>): Promise<string> { 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; } private async processDependenciesInOtherFolders( zipContent: JSZip, visitedAsset: Set<string>, versionMap:Map<string,boolean>, folderName: string, allRefMaps: Map<string, Map<string, boolean>> ): Promise<string> { 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; } private async processDependencyFiles(zipContent: JSZip, visitedAsset: Set<string>,versionMap:Map<string,boolean>): Promise<string> { 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; } private shouldProcessFile(fileName: string): boolean { return (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) && !fileName.includes('resources'); } private shouldProcessFilesInOtherFolders(entry: JSZip.JSZipObject, fileName: string, folderName: string): boolean { return !entry.dir && !fileName.startsWith(folderName + path.sep) && (fileName.endsWith('.yaml') || fileName.endsWith('.yml')); } public async createVersionProcessingMap(buffer: Buffer) { const refMap = new Map<string, boolean>(); 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 private async processFileContent(entry: any, refMap: Map<string, boolean>) { try { const content = await entry.async('string'); const yamlContents = this.parseYaml(content); this.processYamlContents(yamlContents, refMap); } catch (err) { console.log(`Error parsing YAML: ${err}`); } } private parseYaml(content: string): YamlContent[] { try { return yaml.loadAll(content) as YamlContent[]; } catch (err) { console.log(`Error parsing YAML: ${err}`); return []; } } private processYamlContents(yamlContents: YamlContent[], refMap: Map<string, boolean>) { for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent['metadata'] as Metadata_Ref; if (typeof metadata.version === 'string') { updateMapWithMetadata(yamlContent, refMap); } } } } private async processYamlContent(content: string, visitedAsset: Set<string>,versionMap:Map<string,boolean>): Promise<string> { let yamlResult = ''; try { const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent.metadata as Metadata_Ref; 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; } private async processYamlContentForOtherFolders(content: string, visitedAsset: Set<string>,versionMap:Map<string,boolean>, refMap: Map<string, boolean>): Promise<string> { let yamlResult = ''; try { const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { const metadata = yamlContent.metadata as Metadata_Ref; 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: Buffer): Promise<JSZip | null> { 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: Buffer): Promise<GatewaysJson> { console.log('Extracting gateways.json'); const zipContent = await this.loadZipFromBuffer(buffer); return this.extractGatewaysJsonFromZip(zipContent); } private async extractGatewaysJsonFromZip(zipContent: JSZip): Promise<GatewaysJson> { console.log('Extracting gateways.json from ZIP'); let gatewaysJsonContent: GatewaysJson = {} as GatewaysJson; 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; } private findGatewaysJsonFile(zipContent: JSZip): JSZip.JSZipObject | null { 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; } private async parseJsonContent(file: JSZip.JSZipObject): Promise<GatewaysJson> { console.log('Parsing JSON content'); let jsonContent: GatewaysJson = {} as GatewaysJson; try { const content = await file.async('string'); jsonContent = JSON.parse(content); } catch (err) { console.log('parsing JSON content', `${err}`); } return jsonContent; } }