@apistudio/apim-cli
Version:
CLI for API Management Products
345 lines • 12.5 kB
JavaScript
/**
* 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