UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

311 lines (285 loc) 10.9 kB
import JSZip, { JSZipObject } from 'jszip'; import yaml from 'js-yaml'; import { Api_Spec_Ref, SpecObject, YamlContent, ReferenceValidationResult } from '../model/interface.js'; 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 { private isYamlFileForFolder(entry: JSZipObject, folderName: string): boolean { return ( !entry.dir && entry.name.startsWith(folderName + path.sep) && checkFileExtension(entry.name)); } private async loadZipFromBuffer(fileBuffer: Buffer): Promise<JSZip> { LogWrapper.logInfo('0003', 'Loading ZIP from buffer.'); const zip = new JSZip(); return zip.loadAsync(fileBuffer); } private async createProjectAssetReferenceMap(buffer: Buffer, folderName: string, allFolderNames: Set<string>): Promise<Map<string, boolean>> { LogWrapper.logInfo('0351', 'asset', `${folderName}`); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map<string, boolean>(); 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; } private async processYamlFiles( zipContent: JSZip, folderName: string, refMap: Map<string, boolean>, versionMap: Map<string, boolean>, processDependencies = true ): Promise<void> { 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); } }) ); } private async processYamlFile( entry: JSZipObject, refMap: Map<string, boolean>, processDependencies: boolean, versionMap: Map<string, boolean>, ): Promise<void> { LogWrapper.logInfo('0353', entry.name); const content = await entry.async('string'); try { const yamlContents = yaml.loadAll(content) as YamlContent[]; 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}`); } } private updateReferenceMap( yamlContent: YamlContent, refMap: Map<string, boolean>, processDependencies: boolean, versionMap: Map<string, boolean>, ): void { if (processDependencies) { this.extractKey(yamlContent, refMap, '$ref', versionMap, processRef); } updateMapWithMetadata(yamlContent, refMap); } private extractKey( yamlContent: YamlContent, refMap: Map<string, boolean>, keyToExtract: string, versionMap: Map<string, boolean>, transformValue?: (value: string) => string ): void { const extract = (obj: SpecObject) => { 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) as SpecObject); } public async createProjectPathReferenceMap(buffer: Buffer, folderName: string): Promise<Map<string, boolean>> { LogWrapper.logInfo('0351', 'path', folderName); const zipContent = await this.loadZipFromBuffer(buffer); const refMap = new Map<string, boolean>(); 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) as YamlContent[]; 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; } private async validateApiSpecVaraible(buffer: Buffer, folderName: string): Promise<boolean> { 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; } private async checkYamlContent(content: string, fileName: string): Promise<boolean> { LogWrapper.logInfo('0357', AppConstants.YAMLContent, fileName); try { const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (isValidAsset(yamlContent) && yamlContent.kind?.toLowerCase() === 'api') { const specObj = JSON.stringify(yamlContent.spec); const apiSpec = yaml.load(specObj) as Api_Spec_Ref; 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; } private isInvalidApiSpec(apiSpec: Api_Spec_Ref): boolean { const apiSpecField = AppConstants.apiSpec; const apiSpecPathLength = apiSpec[apiSpecField]?.$path?.length ?? 0; return !apiSpec || !apiSpec[apiSpecField] || !apiSpec[apiSpecField].$path || (apiSpecPathLength <= 0); } public async validateProjectAssetReference(buffer: Buffer, folderName: string, allFolderNames: Set<string>): Promise<ReferenceValidationResult> { LogWrapper.logInfo('0355', 'project asset references', folderName); let refMap = new Map<string, boolean>(); 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 }; } } public async validateProjectPathReference(buffer: Buffer, folderName: string, filePathsInFolder: Set<string>): Promise<boolean> { 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; } } public async validateDeploymentAsset(buffer: 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) as YamlContent[]; 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; } public async validateProjectHasMinimumAssets(buffer: Buffer) { LogWrapper.logInfo('0003', 'Validating project has minimum assets.'); return await validateMinAssets(buffer) && await this.validateDeploymentAsset(buffer); } public async validateProjectApiSpecVariable(buffer: Buffer, folderName: string) { LogWrapper.logInfo('0355', 'project API spec variables', folderName); return this.validateApiSpecVaraible(buffer, folderName); } public async validateYaml(buffer: Buffer) { LogWrapper.logInfo('0003', 'Validating YAML files.'); return validateYamlFiles(buffer); } }