@apistudio/apim-cli
Version:
CLI for API Management Products
364 lines (328 loc) • 11.1 kB
text/typescript
/**
* Copyright Super iPaaS Integration LLC, an IBM Company 2024
*/
import { Metadata } from "@apic/api-model/common/Metadata.js";
import AdmZip from "adm-zip";
import fs from "fs";
import { AssetCache } from "../../cache/asset-cache.js";
import { COLON, COMMA } from "../../constants/app-constants.js";
import { AssetCacheModel } from "../../model/asset-cache-model.js";
import { BaseAsset } from "../../model/assets-model.js";
import { equalsIgnoreCase, isNullOrUndefined } from "../common/data-helper.js";
import {
getRandomFileName,
getSubDirectory,
isDirectory,
isDirOrFileExists,
isYamlFile,
normalizePath,
readFile,
} from "../common/fs-helper.js";
import { showError, showInfo, showWarning } from "../common/message-helper.js";
import { readMultiYaml, readYaml } from "../common/yaml-helper.js";
import {
checkForDependencyAssets,
loadCacheWithProject,
} from "./asset-cache-helper.js";
import { getTargetModelAssetKind, isValidAsset } from "./asset-helper.js";
import { getOtherProjectsNames } from "./root-dir-helper.js";
import { DebugManager } from "../../debug/debug-manager.js";
import {
ADDING_DEPENDENCY,
ASSERT_ADDED,
ASSET_DEPENDENCIES,
DEPENDENT_ASSETS_TO_BE_PROCESSED,
DUPLICATE_ENTRIES_FOR_KIND,
FOLLOWING,
INSIDE_THE_PROJECT_PATH,
INVALID_DIRECTORY,
IS_FOUND_IN,
KIND,
METADATA_NAME,
NAME,
NO_ENTRIES_FOUND_FOR_KIND,
NO_FURTHER_DEPENDENCY,
REF,
SEARCHING,
THERE_ARE,
VERSION,
} from "../../constants/message-constants.js";
import { KindEnums } from "@apic/api-model/common/StudioEnums.js";
import { bundleApiDependency } from './api-build-helper.js';
const addDependencyAsset = (
file: fs.Dirent,
zip: AdmZip,
fileExtension = ".yml"
) => {
if (isNullOrUndefined(file)) {
return;
}
const fileName = getRandomFileName(fileExtension);
const filePath = normalizePath(`${file.parentPath}/${file.name}`);
zip.addLocalFile(filePath, "dependencies", fileName);
};
const hasAssetInGivenAssets = (
assets: BaseAsset[],
metadataToSearch: Metadata,
kindToSearch: string
) => {
for (const asset of assets) {
if (!isValidAsset(asset)) {
continue;
}
if (
isSameAsset(asset.metadata, metadataToSearch) &&
equalsIgnoreCase(kindToSearch, getTargetModelAssetKind(asset.kind))
) {
return true;
}
}
return false;
};
// search for the dependency asset for the given asset ref value and project directory path
const searchAsset = (
kindToSearch: string,
assetRefValueToSearch: string,
projectDirPath: string
): fs.Dirent | undefined => {
if (!isDirOrFileExists(projectDirPath) || !isDirectory(projectDirPath)) {
throw new Error(`${INVALID_DIRECTORY} ${projectDirPath}`);
}
const entries: fs.Dirent[] = fs.readdirSync(projectDirPath, {
withFileTypes: true,
recursive: true,
});
const metadataToSearch = fromAssetRefValue(assetRefValueToSearch);
const filteredEntries = entries.filter((entry) => {
if (entry.isDirectory()) {
return false;
}
if (!isYamlFile(entry.name)) {
return false;
}
const assets = readMultiYaml<BaseAsset>(
normalizePath(`${entry.parentPath}/${entry.name}`),
readFile(entry.parentPath, entry.name)
);
return hasAssetInGivenAssets(assets, metadataToSearch, kindToSearch);
});
if (
filteredEntries.length > 1 &&
DebugManager.getInstance().isDebugEnabled()
) {
showWarning(
`${DUPLICATE_ENTRIES_FOR_KIND} - '${kindToSearch}', ${METADATA_NAME} '${metadataToSearch.name}' ${IS_FOUND_IN} '${projectDirPath}'`
);
}
if (
filteredEntries.length === 0 &&
DebugManager.getInstance().isDebugEnabled()
) {
showWarning(
`${NO_ENTRIES_FOUND_FOR_KIND} - '${kindToSearch}', ${METADATA_NAME} '${metadataToSearch.name}' ${IS_FOUND_IN} '${projectDirPath}'`
);
return undefined;
}
return filteredEntries[0];
};
const isSameAsset = (input1: Metadata, input2: Metadata): boolean => {
const isNamespaceAndNameEqual =
input1.namespace === input2.namespace && input1.name === input2.name;
const isVersionEqual = (() => {
const version1 = Number(input1.version);
const version2 = Number(input2.version);
if (Number.isNaN(version1) && Number.isNaN(version2)) {
return input1.version === input2.version;
}
return version1 === version2;
})();
return isNamespaceAndNameEqual && isVersionEqual;
};
const fromAssetRefValue = (assetRefValue: string): Metadata => {
const split = assetRefValue.split(COLON);
if (split.length === 1) {
return {
name: split[0],
};
} else if (split.length === 2) {
return {
name: split[0],
version: split[1],
};
}
return {
namespace: split[0],
name: split[1],
version: split[2],
};
};
const searchAndBundleDependency = (
cachedUnProcessedAsset: AssetCacheModel,
rootDirPath: string,
projects: Set<string>,
zipFile: AdmZip
) => {
try {
for (const project of projects) {
const projectDirPath = getSubDirectory(rootDirPath, project);
if (DebugManager.getInstance().isDebugEnabled()) {
showInfo(
`\n\n ${SEARCHING}: ${KIND} - ${cachedUnProcessedAsset.kind} ${REF} - ${cachedUnProcessedAsset.ref} ${INSIDE_THE_PROJECT_PATH} '${projectDirPath}'`
);
}
const result = searchAsset(
cachedUnProcessedAsset.kind,
cachedUnProcessedAsset.ref,
projectDirPath
);
if (!isNullOrUndefined(result)) {
const fileContent = readFile(
(result as fs.Dirent).parentPath,
(result as fs.Dirent).name
);
const asset = readYaml<BaseAsset>(fileContent);
const isApiKind = equalsIgnoreCase(
getTargetModelAssetKind(asset.kind),
KindEnums.API
);
/* (*) add dependency to zip file*/
if (DebugManager.getInstance().isDebugEnabled()) {
showInfo(
`${ADDING_DEPENDENCY}: ${KIND}-'${asset.metadata.namespace}', ${NAME}-'${asset.metadata.name}', ${VERSION}-'${asset.metadata.version}'`
);
}
if (isApiKind) {
bundleApiDependency(asset, result as fs.Dirent, cachedUnProcessedAsset, rootDirPath, project, zipFile)
} else {
// Non-API dependencies go to dependencies folder (existing behavior)
addDependencyAsset(result as fs.Dirent, zipFile);
showInfo(
`${ASSERT_ADDED} ${asset.metadata.namespace}:${asset.metadata.name}:${asset.metadata.version}`
);
}
/* (*) mark the added asset as processed */
AssetCache.getInstance().markAsProcessed(asset);
/* (*) check for any further dependencies from the current asset */
// Pass the source project to maintain the dependency chain
const sourceProjectForNestedDeps = cachedUnProcessedAsset.sourceProject || project;
checkForDependencyAssets(asset, sourceProjectForNestedDeps);
return;
}
}
/* (*) if there are no assets found, mark this unprocessed asset as checked. */
AssetCache.getInstance().markUnProcessedAssetAsChecked(
cachedUnProcessedAsset
);
} catch (error) {
throw new Error(
`Failure in search asset: kind - ${cachedUnProcessedAsset.kind} ref - ${cachedUnProcessedAsset.ref
} with error: ${(error as Error).message}`
);
}
};
const loadDependenciesFromProjects = (
rootDirPath: string,
projects: Set<string>,
zipFile: AdmZip
) => {
const newlyAddedUnProcessedAssets =
AssetCache.getInstance().getNewlyAddedUnProcessedAssets();
for (const newlyAddedUnProcessedAsset of newlyAddedUnProcessedAssets) {
searchAndBundleDependency(
newlyAddedUnProcessedAsset,
rootDirPath,
projects,
zipFile
);
}
};
const checkAndLoadDependenciesFromProjects = (
rootDirPath: string,
projects: Set<string>,
zipFile: AdmZip
) => {
while (!haveCheckedUnProcessedAssets() && haveUnCheckedUnProcessedAssets()) {
const unProcessedAssets =
AssetCache.getInstance().getNewlyAddedUnProcessedAssets();
if (DebugManager.getInstance().isDebugEnabled()) {
showInfo(
`\n\n ${THERE_ARE} ${unProcessedAssets.size} ${DEPENDENT_ASSETS_TO_BE_PROCESSED}`
);
// logging newly added dependencies
showInfo(`${FOLLOWING} ${unProcessedAssets.size} ${ASSET_DEPENDENCIES} `);
}
unProcessedAssets.forEach((unProcessedAsset) => {
if (DebugManager.getInstance().isDebugEnabled()) {
showInfo(
`${KIND}: '${unProcessedAsset.kind}' ${REF}: ${unProcessedAsset.ref}`
);
}
});
loadDependenciesFromProjects(rootDirPath, projects, zipFile);
}
};
const checkCacheState = () => {
if (haveCheckedUnProcessedAssets()) {
const unProcessedAssets =
AssetCache.getInstance().getCheckedUnProcessedAssets();
showError(
`Following ${unProcessedAssets.size} asset dependencies cannot be resolved:`
);
unProcessedAssets.forEach((unProcessedAsset) =>
showError(`kind: '${unProcessedAsset.kind}' ref: ${unProcessedAsset.ref}`)
);
throw new Error("Dependency assets cannot be resolved");
}
if (!haveUnCheckedUnProcessedAssets()) {
const unProcessedAssets =
AssetCache.getInstance().getNewlyAddedUnProcessedAssets();
if (unProcessedAssets.size === 0) {
if (DebugManager.getInstance().isDebugEnabled()) {
showInfo(`${NO_FURTHER_DEPENDENCY}`);
}
return;
}
}
};
const haveCheckedUnProcessedAssets = () => {
return AssetCache.getInstance().getCheckedUnProcessedAssets().size > 0;
};
const haveUnCheckedUnProcessedAssets = () => {
return AssetCache.getInstance().getNewlyAddedUnProcessedAssets().size > 0;
};
const checkAndLoadDependencies = (
rootDirPath: string,
projectNames: string,
zipFile: AdmZip,
excludeCurrProj = false
) => {
/* (*) Parse the current project assets and update cache with processed and to be processed */
AssetCache.getInstance().clear();
loadCacheWithProject(zipFile);
/* (*) Check the current projects before checking in the other projects */
if (!excludeCurrProj) {
const currentProjects = new Set<string>(projectNames.split(COMMA));
checkAndLoadDependenciesFromProjects(rootDirPath, currentProjects, zipFile);
/* (*) mark unprocessed asset if any as unchecked to make to search in other projects */
AssetCache.getInstance().markAllUnProcessedAssetAsUnchecked();
}
/* (*) Check and load refers to the dependencies directory */
const otherProjects: Set<string> | null = getOtherProjectsNames(
rootDirPath,
projectNames
);
if (!otherProjects) {
showInfo('Skip checking files in other projects as no other projects found');
return;
}
checkAndLoadDependenciesFromProjects(rootDirPath, otherProjects, zipFile);
/* (*) Check for cache state and throw error if some assets cannot be resolved*/
checkCacheState();
};
export {
addDependencyAsset,
checkAndLoadDependencies,
fromAssetRefValue,
isSameAsset,
searchAsset,
};