UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

437 lines 21.3 kB
import { loadYaml } from '@apic/studio-shared'; import { ProjectAssetValidator } from './validator/asset-validator.js'; import JSZip from 'jszip'; import yaml from 'js-yaml'; import { checkFileExtension, convertNumberToString, isValidAsset, updateMapWithMetadata, updateRefs, } from './utils.js'; import path from 'path'; import { DataPowerAdapter } from './adapter/datapower-adapter.js'; import { Logger } from '@apic/studio-shared'; import { errorsArray } from './utils.js'; import { AssetModelKindConstants } from '@apic/studio-client-model'; export class BuildProjectAssets { async loadZipFromBuffer(fileBuffer) { Logger.info('Loading ZIP from buffer'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } async validate(fileBuffer) { Logger.info('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 assetUniqueness = await AssetValidator.validateAssetUniqueness(fileBuffer); const isValid = assetUniqueness && validationResults.every((result) => result.isValid); const allYamlErrors = []; // create a map with folderName and it's refMap const allRefMaps = new Map(); validationResults.forEach((result, index) => { const folderName = Array.from(folderNames)[index]; if (folderName) { allRefMaps.set(folderName, result.refMap); } if (result.errors && result.errors.length > 0) { allYamlErrors.push(...result.errors); } }); return { isValid, allRefMaps, errors: allYamlErrors }; } async extractFolderNamesAndPaths(fileBuffer, folderNames, filePathsInFolder) { Logger.info('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 && folderName !== 'dependencies') { folderNames.add(folderName); filePathsInFolder.add(fileName); } } } async validateFolder(fileBuffer, folderName, AssetValidator, filePathsInFolder, allFolderNames) { //reseting this to 0 as it has older errors as it is. errorsArray.length = 0; 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 soapGatewayValid = await AssetValidator.validateSoapApiGatewayRestriction(fileBuffer); // const yamlValidationResult = await AssetValidator.validateYaml(fileBuffer); // const yamlValid = yamlValidationResult.isValid; // return isvalid true if all the validations are true // return refMap to consolidate assets later return { isValid: assetReferenceValid.isValid && pathReferenceValid && minimumAssetsValid && apiSpecVariableValid && soapGatewayValid, refMap: assetReferenceValid.refMap, errors: errorsArray.map((e) => e.description), }; } async getFileFromZip(fileBuffer, filePath) { const zipContent = await this.loadZipFromBuffer(fileBuffer); const file = zipContent.file(filePath); if (file) { return file.async('string'); } return null; } async createProjectBuildZip(buffer, allRefMaps, mode) { Logger.info('Creating project build ZIP'); const buildZip = new JSZip(); await this.loadZipFromBuffer(buffer); const folderNames = new Set(); const filePathsInFolder = new Set(); let specToContentMap = new Map(); await this.extractFolderNamesAndPaths(buffer, folderNames, filePathsInFolder); specToContentMap = await this.adaptToDataPower(buffer, specToContentMap); await this.addConsolidatedYAMLs(buildZip, buffer, folderNames, allRefMaps); await this.addReferencedFiles(buildZip, buffer, folderNames, specToContentMap, mode); return buildZip; } async adaptToDataPower(fileBuffer, specToContentMap) { const DPAdapter = new DataPowerAdapter(); const isDataPower = await DPAdapter.checkForDataPowerAssembly(fileBuffer); if (isDataPower) { specToContentMap = await DPAdapter.getDataPowerAssemblyContent(fileBuffer); } return specToContentMap; } async addConsolidatedYAMLs(buildZip, buffer, folderNames, allRefMaps) { Logger.info('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 findMatchingApiMetadataForSpecFile(buffer, specFileName) { const zip = await JSZip.loadAsync(buffer); for (const fileName of Object.keys(zip.files)) { const zipEntry = zip.file(fileName); if (!zipEntry) continue; try { const fileHandle = await zipEntry.async('string'); const parsed = loadYaml(fileHandle); //checks for api file to read the apispecpath in the api file if (parsed?.kind === AssetModelKindConstants.API && parsed?.spec?.['api-spec']?.['$path']) { let apiSpecPath; if (parsed?.metadata?.type == 'SOAP' && parsed?.spec?.['rest-def']?.['$path']) { apiSpecPath = parsed.spec['rest-def']['$path']; } else { apiSpecPath = parsed.spec['api-spec']['$path']; } //compares api-spec file name and apispecpath in api file and returns api metadata on match if (path.basename(apiSpecPath) === path.basename(specFileName)) { const metadata = parsed.metadata || {}; return { namespace: metadata.namespace || '', name: metadata.name || '', version: metadata.version || '', }; } } } catch (err) { console.warn(`Failed to parse ${fileName}:`, err); continue; } } return null; } async addReferencedFiles(buildZip, buffer, folderNames, specToContentMap, _mode) { Logger.info('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) => { let file = await this.getFileFromZip(buffer, path.normalize(`${folderName}/${key}`)); if (file !== null) { if (specToContentMap && specToContentMap.get(key) != undefined) { let existingSpec, dataPowerAssemblySpec; //yaml spec if (checkFileExtension(key)) { existingSpec = yaml.load(file); // get the api metadata from the corresponding api file for the spec file const matchingApiInfo = await this.findMatchingApiMetadataForSpecFile(buffer, key); if (matchingApiInfo) { existingSpec.info['x-ibm-name'] = `${matchingApiInfo.name}`; } else { existingSpec.info['x-ibm-name'] = existingSpec.info.title; } dataPowerAssemblySpec = yaml.load(specToContentMap.get(key) ? yaml.dump(specToContentMap.get(key)) || '' : ''); const mergedSpec = { ...existingSpec, ...dataPowerAssemblySpec }; file = yaml.dump(mergedSpec, { indent: 2 }); } else { //json spec existingSpec = JSON.parse(file); // get the api metadata from the corresponding api file for the spec file const matchingApiInfo = await this.findMatchingApiMetadataForSpecFile(buffer, key); if (matchingApiInfo) { existingSpec.info['x-ibm-name'] = `${matchingApiInfo.name}`; } else { existingSpec.info['x-ibm-name'] = existingSpec.info.title; } dataPowerAssemblySpec = JSON.parse(specToContentMap.get(key) ? JSON.stringify(specToContentMap.get(key)) || '' : ''); const mergedSpec = { ...existingSpec, ...dataPowerAssemblySpec }; file = JSON.stringify(mergedSpec, null, 2); } } buildZip.file(path.normalize(`resources/${folderName}/${key}`), file); } }); await Promise.all(promises); } } async createConsolidatedYaml(buffer, folderName, allRefMaps) { let consolidatedYaml = ''; const visitedAsset = new Set(); try { const zipContent = await this.loadZipFromBuffer(buffer); 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) { Logger.error('Error creating consolidated YAML', err instanceof Error ? err : new Error(String(err))); } return consolidatedYaml; } async processYamlFiles(zipContent, folderName, visitedAsset, versionMap) { let consolidatedYaml = ''; for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !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) { Logger.error('Error processing YAML files', err instanceof Error ? err : new Error(String(err))); } } } return consolidatedYaml; } async processDependenciesInOtherFolders(zipContent, visitedAsset, versionMap, folderName, allRefMaps) { Logger.info('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 (entry && this.shouldProcessFilesInOtherFolders(entry, fileName, folderName)) { try { const content = await entry.async('string'); consolidatedYaml += await this.processYamlContentForOtherFolders(content, visitedAsset, versionMap, refMap); } catch (err) { Logger.error('Error parsing dependency YAML file', err instanceof Error ? err : new Error(String(err))); } } } } } return consolidatedYaml; } async processDependencyFiles(zipContent, visitedAsset, versionMap) { Logger.info('Processing dependency YAML files'); let consolidatedYaml = ''; for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !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) { Logger.error('Error parsing dependency YAML file', err instanceof Error ? err : new Error(String(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 (entry && this.shouldProcessFile(fileName) && !entry.dir) { await this.processFileContent(entry, refMap); } } } catch (err) { Logger.error('Error loading ZIP', err instanceof Error ? err : new Error(String(err)), { code: '0013', }); } return refMap; } async processFileContent(entry, refMap) { try { const content = await entry.async('string'); const yamlContents = this.parseYaml(content); this.processYamlContents(yamlContents, refMap); } catch (err) { Logger.error('Error parsing YAML', err instanceof Error ? err : new Error(String(err)), { code: '0013', }); } } parseYaml(content) { try { return yaml.loadAll(content); } catch (err) { Logger.error('Error parsing YAML', err instanceof Error ? err : new Error(String(err)), { code: '0013', }); 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)) { Logger.info(`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) { Logger.error('Error processing YAML content', err instanceof Error ? err : new Error(String(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) { Logger.error('Error processing YAML content', err instanceof Error ? err : new Error(String(err))); } return yamlResult; } async processProjectZip(fileBuffer, mode) { Logger.info('Processing project ZIP'); const validatedFileBuffer = await this.validate(fileBuffer); if (!validatedFileBuffer.isValid) { Logger.info('Project ZIP validation failed'); //Collecting yaml errors return { zip: null, errors: validatedFileBuffer.errors }; } const zip = await this.createProjectBuildZip(fileBuffer, validatedFileBuffer.allRefMaps, mode); return { zip, errors: [] }; } async extractGatewaysJson(buffer) { Logger.info('Extracting gateways.json'); const zipContent = await this.loadZipFromBuffer(buffer); return this.extractGatewaysJsonFromZip(zipContent); } async extractGatewaysJsonFromZip(zipContent) { Logger.info('Extracting gateways.json from ZIP'); let gatewaysJsonContent = {}; try { const gatewaysJsonFile = this.findGatewaysJsonFile(zipContent); if (gatewaysJsonFile) { gatewaysJsonContent = await this.parseJsonContent(gatewaysJsonFile); } else { Logger.info('gateways.json file not found in ZIP'); } } catch (err) { Logger.error('Error extracting gateways.json', err instanceof Error ? err : new Error(String(err))); } return gatewaysJsonContent; } findGatewaysJsonFile(zipContent) { Logger.info('Finding gateways.json file in ZIP'); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !entry.dir && fileName.includes('gateways.json')) { return entry; } } return null; } async parseJsonContent(file) { Logger.info('Parsing JSON content'); let jsonContent = {}; try { const content = await file.async('string'); jsonContent = JSON.parse(content); } catch (err) { Logger.error('Error parsing JSON content', err instanceof Error ? err : new Error(String(err))); } return jsonContent; } } //# sourceMappingURL=build-project-assets.js.map