UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

364 lines (328 loc) 11.1 kB
/** * 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, };