@apistudio/apim-cli
Version:
CLI for API Management Products
189 lines (188 loc) • 7 kB
JavaScript
/**
* 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();