UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

345 lines 12.5 kB
/** * Copyright IBM Corp. 2024, 2025 */ import yaml from 'js-yaml'; import JSZip from 'jszip'; import { AppConstants } from './constants/app.constants.js'; import path from 'path'; import { Logger } from '@apic/studio-shared'; export const createAssetReferenceMap = async (buffer) => { const zip = new JSZip(); const refMap = new Map(); try { const zipContent = await zip.loadAsync(buffer); //logDebug('0308', `${Object.keys(zipContent.files).length}`); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !entry.dir && (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) && !fileName.includes('resources')) { const content = await entry.async('string'); //logInfo('0353', fileName); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { extractRefs(yamlContent, refMap); updateMapWithMetadata(yamlContent, refMap); } } catch { //logError('0013', 'parsing YAML', `${err}`); } } } } catch { //logError('0013', 'loading ZIP', `${err}`); } return refMap; }; export const extractRefs = (yamlContent, refMap) => { const extractRef = (obj) => { for (const key in obj) { const value = obj[key]; if (key === '$ref' && typeof value === 'string') { if (!refMap.has(value)) { refMap.set(value, false); //logDebug('0309', value); } } else if (typeof value === 'object' && value !== null) { extractRef(value); } } }; const specOb = JSON.stringify(yamlContent.spec); extractRef(yaml.load(specOb)); }; export const updateMapWithMetadata = (yamlContent, refMap) => { const metadata = yamlContent['metadata']; const keyParts = []; if (metadata.namespace) { keyParts.push(metadata.namespace); } if (metadata.name) { keyParts.push(metadata.name); } const version = convertNumberToString(metadata.version); if (metadata.version) { keyParts.push(version); } const key = keyParts.join(':'); refMap.set(key, true); //logDebug('0310', 'Metadata', key); }; export const convertNumberToString = (data) => { if (typeof data === 'string') { return data.trim(); } else if (Math.abs(data - Math.floor(data)) < 1e-7) { return data.toFixed(1); } else { return data.toString(); } }; export const isValidAsset = (yamlContent) => { return !!(yamlContent?.kind && yamlContent.kind.toLowerCase() !== AppConstants.TEST && yamlContent.kind.toLowerCase() !== AppConstants.ASSERTION && yamlContent.kind.toLowerCase() !== AppConstants.ENVIRONMENT && yamlContent.kind.toLowerCase() !== AppConstants.MCPTOOL && yamlContent.kind.toLowerCase() !== AppConstants.MCPSERVER && yamlContent?.metadata?.name && yamlContent?.metadata?.version && yamlContent?.spec); }; export const errorsArray = []; export const addErrorToResponse = (errorCode, field, description) => { errorsArray.push({ code: errorCode, field: field, description: description, }); //logDebug('0363', description); }; export const constructErrorResponse = () => { const tempErrorsArray = [...errorsArray]; errorsArray.length = 0; return { respCode: 400, message: 'Invalid Assets or Reference in the Zip', Endpoints: [], errors: tempErrorsArray, }; }; export const createPathReferenceMap = async (buffer) => { const zip = new JSZip(); const refMap = new Map(); try { const zipContent = await zip.loadAsync(buffer); //logDebug('0308', `${Object.keys(zipContent.files).length}`); for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !entry.dir && (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) && !fileName.includes('resources')) { const content = await entry.async('string'); //logInfo('0353', fileName); try { const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { extractPath(yamlContent, refMap); } } catch { //logError('0013', 'parsing YAML', `${err}`); } } } } catch { //logError('0013', 'loading ZIP', `${err}`); } return refMap; }; export const extractPath = (yamlContent, refMap) => { const buildPathRefMap = (obj) => { for (const key in obj) { const value = obj[key]; if (key === '$path' && typeof value === 'string') { if (!refMap.has(value)) { refMap.set(path.normalize(value), false); //logDebug('0309', value); } } else if (typeof value === 'object' && value !== null) { buildPathRefMap(value); } } }; const specOb = JSON.stringify(yamlContent.spec); buildPathRefMap(yaml.load(specOb)); }; export const updatePathRefMap = async (buffer, refMap) => { const zip = await JSZip.loadAsync(buffer); //logDebug('0003', 'Updating path references from resources directory.'); zip.forEach((relativePath) => { if (relativePath.startsWith('resources/')) { const modifiedFileName = relativePath.replace('resources/', ''); if (refMap.has(modifiedFileName)) { refMap.set(modifiedFileName, true); //logDebug('0310', 'Path', modifiedFileName); } } }); }; export const validateMinAssets = async (buffer) => { const zip = new JSZip(); let valid = false; try { const zipContent = await zip.loadAsync(buffer); //logDebug('0308', `${Object.keys(zipContent.files).length}`); for (const fileName in zipContent.files) { if (fileName.endsWith('.yaml') || fileName.endsWith('.yml')) { valid = true; break; } } } catch (err) { //logError('0013', 'loading zip with minimum assets', `${err}`); addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', `Error loading zip with minimum assets: ${err}`); valid = false; } return valid; }; export const updateRefs = (yamlContent, versionMap) => { const updateRef = (obj, verionMap) => { for (const key in obj) { const value = obj[key]; if (key === '$ref' && typeof value === 'string') { if (!verionMap.get(value)) { obj[key] = processRef(value); } else { obj[key] = value; } } else if (typeof value === 'object' && value !== null) { updateRef(value, verionMap); } } }; updateRef(yamlContent.spec, versionMap); return yamlContent; }; export const processRef = (value) => { const parts = value.split(':'); const lastPart = parts[parts.length - 1]; if (lastPart) { const numberValue = parseFloat(lastPart); if (!isNaN(numberValue)) { parts[parts.length - 1] = convertNumberToString(numberValue); } } return parts.join(':'); }; export function checkFileExtension(name) { return name.endsWith('.yml') || name.endsWith('.yaml'); } export function isRelativePath(file) { return file.startsWith('./') || file.startsWith('../'); } export async function extractGatewayTypes(fileBuffer) { try { Logger.info('Extracting gateway types'); const zip = new JSZip(); const zipContent = await zip.loadAsync(fileBuffer); const gatewaysFile = zipContent.file('gateways.json'); if (!gatewaysFile) { Logger.warn('gateways.json not found in the project'); return []; } const gatewaysContent = await gatewaysFile.async('string'); const gatewaysData = JSON.parse(gatewaysContent); const gatewayTypes = []; if (gatewaysData && gatewaysData.gateways && Array.isArray(gatewaysData.gateways)) { for (const gateway of gatewaysData.gateways) { if (gateway.gatewayTypes && Array.isArray(gateway.gatewayTypes)) { gateway.gatewayTypes.forEach((type) => { if (!gatewayTypes.includes(type)) { gatewayTypes.push(type); } }); } } } return gatewayTypes; } catch (error) { Logger.error('Error extracting gateway types', error instanceof Error ? error : new Error(String(error))); return []; } } /** * Checks if an API is a SOAP API by examining its metadata type * @param yamlContent - The parsed YAML content of an API * @returns true if the API is a SOAP API, false otherwise */ export function isSoapApi(yamlContent) { try { if (yamlContent.kind?.toLowerCase() !== 'api') { return false; } if (yamlContent.metadata?.type?.toUpperCase() === 'SOAP') { return true; } return false; } catch (error) { Logger.error('Error checking if API is SOAP', error instanceof Error ? error : new Error(String(error))); return false; } } /** * Validates that SOAP APIs are only deployed to DataPower v5/v6 gateways * @param fileBuffer - The ZIP file buffer containing the project * @returns Object with validation result and errors */ export async function validateSoapGatewayRestriction(fileBuffer) { try { Logger.info('Validating SOAP API gateway restrictions'); const errors = []; const gatewayTypes = await extractGatewayTypes(fileBuffer); if (!gatewayTypes || gatewayTypes.length === 0) { Logger.info('No gateway types found, skipping SOAP validation'); return { isValid: true, errors: [] }; } // Check if any SOAP APIs exist in the project const zip = new JSZip(); const zipContent = await zip.loadAsync(fileBuffer); let hasSoapApi = false; for (const fileName in zipContent.files) { const entry = zipContent.files[fileName]; if (entry && !entry.dir && checkFileExtension(fileName)) { const content = await entry.async('string'); const yamlContents = yaml.loadAll(content); for (const yamlContent of yamlContents) { if (isSoapApi(yamlContent)) { hasSoapApi = true; Logger.info(`SOAP API detected in file: ${fileName}`); break; } } if (hasSoapApi) break; } } // If no SOAP APIs found, validation passes if (!hasSoapApi) { Logger.info('No SOAP APIs found in project'); return { isValid: true, errors: [] }; } const hasDataPowerGateway = gatewayTypes.some((type) => AppConstants.SOAP_ALLOWED_GATEWAYS.includes(type.toLowerCase())); if (!hasDataPowerGateway) { const errorMessage = AppConstants.ERROR_SOAP_GATEWAY_RESTRICTION; Logger.error(errorMessage); errors.push(errorMessage); return { isValid: false, errors }; } Logger.info('SOAP API gateway validation passed'); return { isValid: true, errors: [] }; } catch (error) { const errorMessage = 'Error validating SOAP API gateway restrictions'; Logger.error(errorMessage, error instanceof Error ? error : new Error(String(error))); return { isValid: false, errors: [errorMessage], }; } } //# sourceMappingURL=utils.js.map