UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

346 lines (322 loc) 11.8 kB
/** * Copyright Super iPaaS Integration LLC, an IBM Company 2024 */ import yaml from 'js-yaml'; import { Api_Spec_Ref, Metadata_Ref, YamlContent } from './model/interface.js'; import { GatewaysJson } from '@apic/studio-shared'; import { AppConstants } from './constants/app.constants.js'; import { addErrorToResponse, convertNumberToString, createAssetReferenceMap, createPathReferenceMap, isValidAsset, updatePathRefMap, validateMinAssets, validateYamlFiles } from './utils.js'; import JSZip from 'jszip'; // import { // } from './service/log-wrapper.js'; export class ZipProcessor { buffer: Buffer; constructor(buffer: Buffer) { this.buffer = buffer; } async validate(): Promise<boolean> { //.logInfo('0003', 'Starting validation process for ZIP buffer'); // Starting validation process if (!await this.validateReferences()) { //.logDebug('0360', 'references'); return false; } if (!await this.validatePaths()) { //.logDebug('0360', 'paths'); return false; } if (!await this.validateZip()) { //.logDebug('0360', 'zip'); return false; } if (!await this.validateApiFiles()) { //.logDebug('0360', 'API files'); return false; } if (!await this.validateYamlStructure()) { //.logDebug('0360', AppConstants.YAMLStructure); return false; } return this.validateYamlFiles(); } private async validateYamlStructure(): Promise<boolean> { //.logDebug('0361', AppConstants.YAMLStructure); if (!await validateYamlFiles(this.buffer)) { //.logError('0255', 'Invalid YAML files detected in buffer'); return false; } //.logDebug('0362', AppConstants.YAMLStructure); return true; } private async validateApiFiles(): Promise<boolean> { //.logDebug('0361', 'API files in zip'); const zip = new JSZip(); try { const zipContent = await zip.loadAsync(this.buffer); //.logDebug('0362', 'API files in zip'); return await this.validateZipContent(zipContent); } catch (err) { //.logError('0013', 'loading ZIP', `${err}`); return false; } } private async validateZipContent(zipContent: JSZip): Promise<boolean> { //.logDebug('0361', 'ZIP content'); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.isYamlFile(entry, fileName)) { //.logDebug('0357', AppConstants.YAMLContent, fileName); const content = await entry.async('string'); const isValid = await this.checkYamlContent(content, fileName); if (!isValid) { //.logError('0359', AppConstants.YAMLContent, fileName); return false; } } } //.logDebug('0362', 'ZIP content'); return true; } private async checkYamlContent(content: string, fileName: string): Promise<boolean> { try { const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (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}`); //.logError('0359', 'API spec', fileName); return false; } } } } catch (err) { //.logError('0013', `parsing YAML content in ${fileName}`, `${err}`); return false; } //.logDebug('0358', AppConstants.YAMLContent, fileName); return true; } private isInvalidApiSpec(apiSpec: Api_Spec_Ref): boolean { //.logDebug('0361', 'API Specification'); const apiSpecField = AppConstants.apiSpec; const apiSpecPathLength = apiSpec[apiSpecField]?.$path?.length ?? 0; return !apiSpec || !apiSpec[apiSpecField] || !apiSpec[apiSpecField].$path || (apiSpecPathLength <= 0); } private async validateReferences(): Promise<boolean> { //.logDebug('0361', 'references'); const refMap = await createAssetReferenceMap(this.buffer); const allRefsValid = Array.from(refMap.entries()).every(([key, value]) => { if (!value) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for Reference ${key}`); //.logError('0359', 'reference', key); } return value; }); if (!allRefsValid) { //.logError('0255', 'Some references are not valid'); return false; } return true; } private async validatePaths(): Promise<boolean> { //.logDebug('0361', 'paths'); const pathMap = await createPathReferenceMap(this.buffer); await updatePathRefMap(this.buffer, pathMap); const allPathsValid = Array.from(pathMap.entries()).every(([key, value]) => { if (!value) { addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for Reference ${key}`); } return value; }); if (!allPathsValid) { //.logError('0255', 'Some references are not valid'); return false; } //.logDebug('0362', 'paths'); return true; } private async validateZip(): Promise<boolean> { //.logDebug('0361', 'ZIP'); if (!await validateMinAssets(this.buffer)) { //.logError('0255', 'Zip does not contain minimum assets'); return false; } //.logDebug('0362', 'ZIP'); return true; } private async validateYamlFiles(): Promise<boolean> { //.logDebug('0361', 'YAML files in zip'); const zip = new JSZip(); const zipContent = await zip.loadAsync(this.buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.isYamlFile(entry, fileName)) { if (!(await this.validateYamlFileContent(entry, fileName))) { return false; } } } //.logDebug('0362', 'YAML files in zip'); return true; } private async validateYamlFileContent(entry: JSZip.JSZipObject, fileName: string): Promise<boolean> { //.logDebug('0357', AppConstants.YAMLContent, fileName); try { const content = await entry.async('string'); const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (!isValidAsset(yamlContent)) { //.logError('0359', 'asset', fileName); addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'YAML_FILE', `Invalid YAML in file ${fileName}`); return false; } } } catch (err) { //.logError('0013', `parsing YAML in ${fileName}`, `${err}`); return false; } //.logDebug('0358', AppConstants.YAMLContent, fileName); return true; } async processZip(): Promise<{ buildZip: JSZip } | null> { //.logInfo('0003', 'Creating build ZIP from buffer'); if (!await this.validate()) { //.logDebug('0003', 'ZIP validation failed during build process'); return null; } //.logDebug('0003', 'Validation passed, extracting kind groups'); const kindGroups = await this.extractKindGroups(this.buffer); //.logDebug('0003', 'Extracted kind groups, creating build ZIP'); const buildZip = await this.createBuildZip(this.buffer, kindGroups); //.logInfo('0003', 'Build ZIP creation completed'); return { buildZip }; } async extractGatewaysJson(buffer: Buffer): Promise<GatewaysJson> { //.logDebug('0003', 'Extracting gateways.json from ZIP'); const zip = new JSZip(); let gatewaysJsonContent: GatewaysJson = {} as GatewaysJson; try { const zipContent = await zip.loadAsync(buffer); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (!entry.dir && fileName.includes('gateways.json')) { try { const content = await entry.async('string'); gatewaysJsonContent = JSON.parse(content); } catch (err) { //.logError('0013', 'parsing gateways.json', `${err}`); } } } } catch (err) { //.logError('0013', 'loading ZIP', `${err}`); } return gatewaysJsonContent; } private async extractKindGroups(buffer: Buffer): Promise<{ [key: string]: string[] }> { const zip = new JSZip(); const kindGroups: { [key: string]: string[] } = {}; const kindGroupsSet: Set<string> = new Set<string>(); try { const zipContent = await zip.loadAsync(buffer); await this.processZipContent(zipContent, kindGroups, kindGroupsSet); } catch (err) { //.logError('0013', 'extracting kind groups', `${err}`); } return kindGroups; } private async processZipContent(zipContent: JSZip, kindGroups: { [key: string]: string[] }, kindGroupsSet: Set<string>): Promise<void> { for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (this.isYamlFile(entry, fileName)) { //.logDebug('0353', fileName); await this.processYamlEntry(entry, fileName, kindGroups, kindGroupsSet); } } } private isYamlFile(entry: JSZip.JSZipObject, fileName: string): boolean { return !entry.dir && (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) && !fileName.startsWith('resources'); } private async processYamlEntry(entry: JSZip.JSZipObject, fileName: string, kindGroups: { [key: string]: string[] }, kindGroupsSet: Set<string>): Promise<void> { try { const content = await entry.async('string'); const yamlContents = yaml.loadAll(content) as YamlContent[]; for (const yamlContent of yamlContents) { if (yamlContent && yamlContent.kind) { await this.processYamlContent(yamlContent, kindGroups, kindGroupsSet); } } } catch (err) { //.logError('0013', 'parsing YAML', `${err}`); } } private async processYamlContent(yamlContent: YamlContent, kindGroups: { [key: string]: string[] }, kindGroupsSet: Set<string>): Promise<void> { const kind = yamlContent.kind; if (!kindGroups[kind]) { kindGroups[kind] = []; } const metadata = yamlContent.metadata as Metadata_Ref; const contentString = `${metadata.namespace?metadata.namespace:''}:${metadata.name}:${convertNumberToString(metadata.version)}`; if (!kindGroupsSet.has(contentString)) { yamlContent.metadata.version = convertNumberToString(metadata.version); const yamlString = yaml.dump(yamlContent); const contentWithYaml = await this.zipYamlContent(yamlString); kindGroups[kind].push(contentWithYaml); kindGroupsSet.add(contentString); } } private async zipYamlContent(yamlString: string): Promise<string> { //.logDebug('0003', 'Zipping YAML content'); const tempZip = new JSZip(); tempZip.file('file.yaml', yamlString); const tempZipBuffer = await tempZip.generateAsync({ type: 'nodebuffer' }); const zipWithYaml = new JSZip(); const yamlZipContent = await zipWithYaml.loadAsync(tempZipBuffer); if (yamlZipContent.files) { return yamlZipContent.files['file.yaml'].async('string'); } //.logError('0004', 'Failed to zip YAML content'); throw new Error('Failed to zip YAML content'); } private async createBuildZip(buffer: Buffer, kindGroups: { [key: string]: string[] }): Promise<JSZip> { const jszip = new JSZip(); const zip = await JSZip.loadAsync(buffer); const resourcesFolder = jszip.folder('resources'); for (const fileName in zip.files) { const entry = zip.files[fileName]; if (!entry.dir) { const content = await entry.async('nodebuffer'); if (fileName.includes('resources/')) { //.logDebug('0003', `Adding file to resources folder: ${fileName}`); resourcesFolder?.file(fileName.replace('resources/', ''), content); } } } const allYamlContent = this.combineYamlContent(kindGroups); jszip.file('gw_build.yaml', Buffer.from(allYamlContent.join('\n---\n') + '\n', 'utf-8')); //.logDebug('0003', 'Build ZIP creation completed'); return jszip; } private combineYamlContent(kindGroups: { [key: string]: string[] }): string[] { //.logDebug('0003', 'Combining YAML content from kind groups'); const allYamlContent: string[] = []; for (const kind in kindGroups) { const combinedYaml = kindGroups[kind].join('\n---\n'); allYamlContent.push(combinedYaml); } //.logDebug('0003', 'YAML content combined'); return allYamlContent; } }