UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

262 lines (261 loc) 12 kB
import JSZip from 'jszip'; import yaml from 'js-yaml'; import { addErrorToResponse, isValidAsset, processRef, updateMapWithMetadata, validateMinAssets, validateYamlFiles, checkFileExtension } from '../utils.js'; import { LogWrapper } from '../service/log-wrapper.js'; import { AppConstants } from '../constants/app.constants.js'; import path from 'path'; import { BuildProjectAssets } from '../build-project-assets.js'; export class ProjectAssetValidator { isYamlFileForFolder(entry, folderName) { return (!entry.dir && entry.name.startsWith(folderName + path.sep) && checkFileExtension(entry.name)); } async loadZipFromBuffer(fileBuffer) { LogWrapper.logInfo('0003', 'Loading ZIP from buffer.'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } async createProjectAssetReferenceMap(buffer, folderName, allFolderNames) { LogWrapper.logInfo('0351', 'asset', `${folderName}`); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map(); try { const obj = new BuildProjectAssets(); const versionMap = await obj.createVersionProcessingMap(buffer); await this.processYamlFiles(zipContent, folderName, refMap, versionMap); await this.processYamlFiles(zipContent, 'dependencies', refMap, versionMap, false); // if unresolved refs are identified after checking in the project and dependencies folders then check in other folders const hasUnresolvedRefs = Array.from(refMap.values()).some(value => !value); if (hasUnresolvedRefs) { for (const otherFolderName of allFolderNames) { if (otherFolderName !== folderName && otherFolderName !== 'dependencies') { await this.processYamlFiles(zipContent, otherFolderName, refMap, versionMap, false); } } } LogWrapper.logInfo('0003', 'Successfully processed YAML files.'); } catch (err) { LogWrapper.logError('0013', 'processing ZIP', `${err}`); } return refMap; } async processYamlFiles(zipContent, folderName, refMap, versionMap, processDependencies = true) { LogWrapper.logInfo('0352', folderName); await Promise.all(Object.keys(zipContent.files).map(async (fileName) => { const entry = zipContent.files[fileName]; if (this.isYamlFileForFolder(entry, folderName)) { await this.processYamlFile(entry, refMap, processDependencies, versionMap); } })); } async processYamlFile(entry, refMap, processDependencies, versionMap) { LogWrapper.logInfo('0353', entry.name); const content = await entry.async('string'); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { this.updateReferenceMap(yamlContent, refMap, processDependencies, versionMap); } } LogWrapper.logInfo('0354', AppConstants.YAMLContent, entry.name); } catch (err) { LogWrapper.logError('0013', `parsing YAML in file ${entry.name}`, `${err}`); } } updateReferenceMap(yamlContent, refMap, processDependencies, versionMap) { if (processDependencies) { this.extractKey(yamlContent, refMap, '$ref', versionMap, processRef); } updateMapWithMetadata(yamlContent, refMap); } extractKey(yamlContent, refMap, keyToExtract, versionMap, transformValue) { const extract = (obj) => { for (const key in obj) { const value = obj[key]; if (key === keyToExtract && typeof value === 'string') { const transformedValue = transformValue ? transformValue(value) : value; if (versionMap.has(value)) { if (!refMap.has(value)) { refMap.set(value, true); } } else { if (!refMap.has(transformedValue)) { refMap.set(transformedValue, false); } } } else if (typeof value === 'object' && value !== null) { extract(value); } } }; const specOb = JSON.stringify(yamlContent.spec); extract(yaml.load(specOb)); } async createProjectPathReferenceMap(buffer, folderName) { LogWrapper.logInfo('0351', 'path', folderName); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map(); try { const obj = new BuildProjectAssets(); const versionMap = await obj.createVersionProcessingMap(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.isYamlFileForFolder(entry, folderName)) { const content = await entry.async('string'); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { this.extractKey(yamlContent, refMap, '$path', versionMap, path.normalize); } } LogWrapper.logInfo('0354', 'path extraction', fileName); } catch (err) { LogWrapper.logError('0013', `parsing YAML in file ${fileName}`, `${err}`); } } } } catch (err) { LogWrapper.logError('0013', 'loading ZIP', `${err}`); } return refMap; } async validateApiSpecVaraible(buffer, folderName) { LogWrapper.logInfo('0355', 'API Spec variable', folderName); const zipContent = await this.loadZipFromBuffer(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (!entry.dir && fileName.startsWith(folderName + path.sep) && checkFileExtension(fileName)) { const content = await entry.async('string'); const isValid = await this.checkYamlContent(content, fileName); if (!isValid) { return false; } } } return true; } async checkYamlContent(content, fileName) { LogWrapper.logInfo('0357', AppConstants.YAMLContent, fileName); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent) && yamlContent.kind?.toLowerCase() === 'api') { const specObj = JSON.stringify(yamlContent.spec); const apiSpec = yaml.load(specObj); if (this.isInvalidApiSpec(apiSpec)) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, fileName, `Validation failed for api-spec field in file ${fileName}`); return false; } } } LogWrapper.logInfo('0358', AppConstants.YAMLContent, fileName); } catch (err) { LogWrapper.logError('0013', `parsing YAML in file ${fileName}`, `${err}`); return false; } return true; } isInvalidApiSpec(apiSpec) { const apiSpecField = AppConstants.apiSpec; const apiSpecPathLength = apiSpec[apiSpecField]?.$path?.length ?? 0; return !apiSpec || !apiSpec[apiSpecField] || !apiSpec[apiSpecField].$path || (apiSpecPathLength <= 0); } async validateProjectAssetReference(buffer, folderName, allFolderNames) { LogWrapper.logInfo('0355', 'project asset references', folderName); let refMap = new Map(); try { refMap = await this.createProjectAssetReferenceMap(buffer, folderName, allFolderNames); const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => { if (!value) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for Reference ${key}`); } return value; }); if (!allRefsValid) { LogWrapper.logError('0255', 'Some references are not valid.'); return { isValid: false, refMap }; } return { isValid: true, refMap }; } catch (err) { LogWrapper.logError('0013', 'validating asset', `${err}`); return { isValid: false, refMap }; } } async validateProjectPathReference(buffer, folderName, filePathsInFolder) { LogWrapper.logInfo('0355', 'project path references', folderName); try { const refMap = await this.createProjectPathReferenceMap(buffer, folderName); refMap.forEach((_, key) => { if (filePathsInFolder.has(path.normalize(`${folderName}/${key}`))) { refMap.set(key, true); } }); const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => { if (!value) { console.log(`Validation failed for path ${key} in ${folderName}`); LogWrapper.logError('0003', `Validation failed for path ${key} in ${folderName}`); addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for path ${key} in ${folderName}`); } return value; }); if (!allRefsValid) { LogWrapper.logError('0255', 'Some references are not valid.'); return false; } return true; } catch (err) { LogWrapper.logError('0013', 'validating asset', `${err}`); return false; } } async validateDeploymentAsset(buffer) { try { const zipContent = await this.loadZipFromBuffer(buffer); LogWrapper.logDebug('0308', `${Object.keys(zipContent.files).length}`); for (const fileName in zipContent.files) { if (checkFileExtension(fileName)) { const entry = zipContent.files[fileName]; const content = await entry.async('string'); const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent)) { return true; } } } } } catch (err) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', `Error loading zip with minimum assets: ${err}`); } addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', 'Error loading zip with minimum assets required for deploymnet'); return false; } async validateProjectHasMinimumAssets(buffer) { LogWrapper.logInfo('0003', 'Validating project has minimum assets.'); return await validateMinAssets(buffer) && await this.validateDeploymentAsset(buffer); } async validateProjectApiSpecVariable(buffer, folderName) { LogWrapper.logInfo('0355', 'project API spec variables', folderName); return this.validateApiSpecVaraible(buffer, folderName); } async validateYaml(buffer) { LogWrapper.logInfo('0003', 'Validating YAML files.'); return validateYamlFiles(buffer); } }