@apistudio/apim-cli
Version:
CLI for API Management Products
267 lines (266 loc) • 9.91 kB
JavaScript
/**
* Copyright Super iPaaS Integration LLC, an IBM Company 2024
*/
import yaml from 'js-yaml';
import JSZip from 'jszip';
import { UpperCaseKinds } from './model/interface.js';
import { AssetValidator } from './service/validation-service.js';
import { AppConstants } from './constants/app.constants.js';
import path from 'path';
export const validateYamlFiles = async (buffer) => {
const zip = new JSZip();
let allValid = true;
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.dir && (fileName.endsWith('.yaml') || fileName.endsWith('.yml'))) {
const content = await entry.async('string');
//logInfo('0353', fileName);
try {
const yamlContents = yaml.loadAll(content);
if (!validatYamlContent(yamlContents, fileName)) {
allValid = false;
}
}
catch (err) {
//logError('0013', 'while parsing YAML', `${err}`);
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, AppConstants.YAML_FILE, `Invalid YAML in file ${fileName}: ${err}`);
allValid = false;
}
}
}
}
catch (err) {
//logError('0013', 'while loading ZIP', `${err}`);
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, 'ZIP_FILE', `Error loading ZIP: ${err}`);
allValid = false;
}
return allValid;
};
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.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 (err) {
//logError('0013', 'parsing YAML', `${err}`);
}
}
}
}
catch (err) {
//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?.metadata?.name &&
yamlContent?.metadata?.version &&
yamlContent?.spec &&
UpperCaseKinds.includes(yamlContent.kind.toUpperCase()));
};
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.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 (err) {
//logError('0013', 'parsing YAML', `${err}`);
}
}
}
}
catch (err) {
//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;
};
const validatYamlContent = (yamlContents, fileName) => {
let allValid = true;
const assetValidator = new AssetValidator();
for (const yamlContent of yamlContents) {
if (yamlContent?.kind != null && !assetValidator.validateAssets(yamlContent)) {
//logError('0361', fileName);
addErrorToResponse(AppConstants.VALIDATION_ERROR_CODE, AppConstants.YAML_FILE, `Invalid YAML in file ${fileName}`);
allValid = false;
}
}
return allValid;
};
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);
}
}
};
//@ts-expect-error: This error is expected because of proto object is of type any or undefined
updateRef(yamlContent.spec, versionMap);
return yamlContent;
};
export const processRef = (value) => {
const parts = value.split(':');
const numberValue = parseFloat(parts[parts.length - 1]);
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('../');
}