UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

215 lines (188 loc) 6.54 kB
/** * Copyright IBM Corp. 2024, 2025 */ /** * Manages all the variable contexts with unique context. */ import { EnvVar, VariableContext } from './variable-context.js'; type EnvInput = | Record<string, any> | Record<string, EnvVar> | EnvVar[] | Array<Record<string, any>>; export class ContextManager { private contexts = new Map<string, VariableContext>(); // This will be accessible for entire private globalContext = new VariableContext(); createContext(contextId: string): VariableContext { if (!this.contexts.has(contextId)) { this.contexts.set(contextId, new VariableContext()); } return this.contexts.get(contextId)!; } getContext(contextId: string): VariableContext { return this.createContext(contextId)!; } deleteContext(contextId: string): void { this.contexts.delete(contextId); } listContexts(): string[] { return [...this.contexts.keys()]; } getGlobalContext(): VariableContext { return this.globalContext; } // Don't use this unless we want to clean context manager clearAll(): void { this.contexts.clear(); this.globalContext.clear(); } loadEnv(contextId: string, envVars: EnvInput): void { const context = this.createContext(contextId); // If it's array of key-value pairs with `key`, `value`, `isSecret` if ( Array.isArray(envVars) && envVars.every((e) => 'key' in e && 'value' in e) ) { for (const env of envVars) { context.setEnvVariable(env.key, env.value, env.isSecret as boolean); } return; } // If it's array of plain objects const envArray = Array.isArray(envVars) ? envVars : [envVars]; for (const env of envArray) { for (const [key, val] of Object.entries(env)) { if ( val && typeof val === 'object' && 'value' in val && 'isSecret' in val ) { context.setEnvVariable(key, val.value, val.isSecret as boolean); } else { context.setEnvVariable(key, val); } } } } // Resolve variable name with the value store in context // which will return the resolved value in the passed input resolve(contextId: string, input: any): any { if (input == undefined) { return input; } const context = this.contexts.get(contextId); if (!context) throw new Error(`Context '${contextId}' not found.`); if (Array.isArray(input)) { return input.map((item) => this.resolve(contextId, item)); } else if (typeof FormData !== 'undefined' && input instanceof FormData) { return input; } else if (typeof input === 'object' && input !== null) { const result: Record<string, any> = {}; for (const [key, value] of Object.entries(input)) { result[key] = this.resolve(contextId, value); } return result; } // for primitive types return this.resolveValue(contextId, input); } // For any string which have expression like "result.0.name" instead of variable // typeof input parameter is set to any, in runtime it should be only string. // to validate unit test, it is set to any resolveExpression(contextId: string, input: any): any { const context = this.contexts.get(contextId); if (!context) throw new Error(`Context '${contextId}' not found.`); if (typeof input !== 'string') { throw new Error(`${input} should be a string expression`); } const result = this.resolvePath(contextId, input); return result; } // ResolveValue function will understand whether the input is variable or statement with variable // based on that, resolveExpression will be invoked. private resolveValue(contextId: string, input: any): any { // If input is non string, then it won't be a variable if (typeof input !== 'string' || !input.includes('${')) { return input; } const fullVar = input.match(/^\$\{([^{}]+)\}$/); if (fullVar) { return this.resolvePath(contextId, fullVar[1]); } return this.replaceRecursiveExpression(contextId, input); } // For mixed string: recursively replace ALL ${...} patterns until none left private replaceRecursiveExpression(contextId: string, input: string) { if (!input) { return input; } while (input.includes('${')) { input = input.replace(/\$\{([^{}]*)\}/g, (_: any, expr: any) => { try { const value = this.resolvePath(contextId, expr); // If value is undefined, return empty string if (value === undefined) { return ''; } // Handle circular structure in JSON stringification return typeof value === 'object' ? this.safeStringify(value) : String(value); } catch (e) { console.error(e); return ''; // Return empty string on error } }); } return input; } // Safely stringify objects handling circular references private safeStringify(obj: any): string { try { // Use a WeakSet to track objects that have been seen const seen = new WeakSet(); return JSON.stringify(obj, (key, value) => { // If value is an object and not null if (typeof value === 'object' && value !== null) { // If we've seen this object before, return a placeholder to avoid circular reference if (seen.has(value)) { return '[Circular Reference]'; } // Add the value to our set of seen objects seen.add(value); } return value; }); } catch { // Fallback if JSON.stringify still fails return '[Complex Object]'; } } private resolvePath(contextId: string, expr: string): any { if (!expr) { return expr; } const context = this.createContext(contextId); const global = this.getGlobalContext(); const [baseKey, ...pathParts] = expr.split('.'); let resolved = context?.getValue(baseKey) ?? global?.getValue(baseKey); if (resolved === undefined) { return undefined; } for (const part of pathParts) { const key = /^\d+$/.test(part) ? Number(part) : part; if (resolved === undefined || resolved === null) { return undefined; } // If the key doesn't exist in the object, return undefined instead of throwing an error if (!(key in resolved)) { return undefined; } resolved = resolved[key]; } return resolved; } } export const VCM = new ContextManager();