UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

189 lines (188 loc) 7 kB
/** * Copyright IBM Corp. 2024, 2025 */ /** * Manages all the variable contexts with unique context. */ import { VariableContext } from './variable-context.js'; export class ContextManager { constructor() { this.contexts = new Map(); // This will be accessible for entire this.globalContext = new VariableContext(); } createContext(contextId) { if (!this.contexts.has(contextId)) { this.contexts.set(contextId, new VariableContext()); } return this.contexts.get(contextId); } getContext(contextId) { return this.createContext(contextId); } deleteContext(contextId) { this.contexts.delete(contextId); } listContexts() { return [...this.contexts.keys()]; } getGlobalContext() { return this.globalContext; } // Don't use this unless we want to clean context manager clearAll() { this.contexts.clear(); this.globalContext.clear(); } loadEnv(contextId, envVars) { 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); } 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); } 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, input) { 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 = {}; 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, input) { 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. resolveValue(contextId, input) { // 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 replaceRecursiveExpression(contextId, input) { if (!input) { return input; } while (input.includes('${')) { input = input.replace(/\$\{([^{}]*)\}/g, (_, expr) => { 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 safeStringify(obj) { 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]'; } } resolvePath(contextId, expr) { 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();