@apistudio/apim-cli
Version:
CLI for API Management Products
307 lines • 14.6 kB
JavaScript
/**
* Copyright IBM Corp. 2024, 2025
*/
import JSZip from 'jszip';
import yaml from 'js-yaml';
import { addErrorToResponse, isValidAsset, processRef, updateMapWithMetadata, validateMinAssets, checkFileExtension, errorsArray, validateSoapGatewayRestriction, } from '../utils.js';
import { Logger } from '@apic/studio-shared';
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) {
Logger.info('Loading ZIP from buffer');
const zip = new JSZip();
return zip.loadAsync(fileBuffer);
}
async validateAssetUniqueness(fileBuffer) {
const zipContent = await this.loadZipFromBuffer(fileBuffer);
const newMap = new Map();
let allValid = true;
await Promise.all(Object.keys(zipContent.files).map(async (fileName) => {
const entry = zipContent.files[fileName];
if (entry && checkFileExtension(entry.name)) {
const content = await entry.async('string');
const yamlContents = yaml.loadAll(content);
for (const yamlContent of yamlContents) {
if (isValidAsset(yamlContent)) {
const namespace = yamlContent.metadata.namespace || 'default';
const name = yamlContent.metadata.name;
const version = yamlContent.metadata.version;
const kind = yamlContent.kind;
const ref = `${namespace}:${name}:${version}`;
if (newMap.has(ref)) {
const existing = newMap.get(ref);
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, fileName, `Duplicate asset detected: '${name}' in namespace '${namespace}' with version '${version}' and kind '${kind}'. First seen in file '${existing?.fileName}'.`);
allValid = false;
}
else {
newMap.set(ref, { fileName, kind });
}
}
}
}
}));
return allValid;
}
async createProjectAssetReferenceMap(buffer, folderName, allFolderNames) {
Logger.info(`Creating asset reference map for folder: ${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);
}
}
}
Logger.info('Successfully processed YAML files');
}
catch (err) {
Logger.error('Error processing ZIP', err instanceof Error ? err : new Error(String(err)));
}
return refMap;
}
async processYamlFiles(zipContent, folderName, refMap, versionMap, processDependencies = true) {
Logger.info(`Processing YAML files in folder: ${folderName}`);
await Promise.all(Object.keys(zipContent.files).map(async (fileName) => {
const entry = zipContent.files[fileName];
if (entry && this.isYamlFileForFolder(entry, folderName)) {
await this.processYamlFile(entry, refMap, processDependencies, versionMap);
}
}));
}
async processYamlFile(entry, refMap, processDependencies, versionMap) {
Logger.info(`Processing YAML file: ${entry.name}`);
const content = await entry.async('string');
try {
const yamlContents = yaml.loadAll(content);
for (const yamlContent of yamlContents) {
if (isValidAsset(yamlContent) &&
!AppConstants.IGNORE_ASSETS_DURING_DEPLOY.includes(yamlContent.kind.toLowerCase())) {
this.updateReferenceMap(yamlContent, refMap, processDependencies, versionMap);
}
}
Logger.info(`Successfully processed YAML content in file: ${entry.name}`);
}
catch (err) {
Logger.error(`Error parsing YAML in file ${entry.name}`, err instanceof Error ? err : new Error(String(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) {
Logger.info(`Creating path reference map for folder: ${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 (entry && 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);
}
}
Logger.info(`Path extraction completed for file: ${fileName}`);
}
catch (err) {
Logger.error(`Error parsing YAML in file ${fileName}`, err instanceof Error ? err : new Error(String(err)));
}
}
}
}
catch (err) {
Logger.error('Error loading ZIP', err instanceof Error ? err : new Error(String(err)));
}
return refMap;
}
async validateApiSpecVaraible(buffer, folderName) {
Logger.info(`Validating API Spec variable in folder: ${folderName}`);
const zipContent = await this.loadZipFromBuffer(buffer);
for (const fileName in zipContent.files) {
const entry = zipContent.files[fileName];
if (entry &&
!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) {
Logger.info(`Checking YAML content in file: ${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;
}
}
}
Logger.info(`Successfully validated YAML content in file: ${fileName}`);
}
catch (err) {
Logger.error(`Error parsing YAML in file ${fileName}`, err instanceof Error ? err : new Error(String(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) {
Logger.info(`Validating project asset references in folder: ${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) {
Logger.error('Some references are not valid');
return {
isValid: false,
refMap,
errors: errorsArray.map((err) => err.description),
};
}
return { isValid: true, refMap, errors: [] };
}
catch (err) {
Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err)));
return {
isValid: false,
refMap,
errors: errorsArray.map((err) => err.description),
};
}
}
async validateProjectPathReference(buffer, folderName, filePathsInFolder) {
Logger.info(`Validating project path references in folder: ${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) {
Logger.error(`Validation failed for path ${key} in ${folderName}`);
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, key, `Validation failed for path ${key} in ${folderName}`);
}
return value;
});
if (!allRefsValid) {
Logger.error('Some references are not valid');
return false;
}
return true;
}
catch (err) {
Logger.error('Error validating asset', err instanceof Error ? err : new Error(String(err)));
return false;
}
}
async validateDeploymentAsset(buffer) {
try {
const zipContent = await this.loadZipFromBuffer(buffer);
Logger.debug(`Processing ${Object.keys(zipContent.files).length} files`);
for (const fileName in zipContent.files) {
const entry = zipContent.files[fileName];
if (entry && checkFileExtension(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) {
Logger.info('Validating project has minimum assets');
return (await validateMinAssets(buffer)) && (await this.validateDeploymentAsset(buffer));
}
async validateProjectApiSpecVariable(buffer, folderName) {
Logger.info(`Validating project API spec variables in folder: ${folderName}`);
return this.validateApiSpecVaraible(buffer, folderName);
}
async validateSoapApiGatewayRestriction(buffer) {
Logger.info('Validating SOAP API gateway restrictions');
const result = await validateSoapGatewayRestriction(buffer);
if (!result.isValid && result.errors.length > 0) {
result.errors.forEach((error) => {
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'SOAP_API_TO_GATEWAY', error);
});
}
return result.isValid;
}
}
//# sourceMappingURL=asset-validator.js.map