import { executeAzCommand, CommandResult } from '../lib/cli-executor.js';
import { withCache, CacheKeys } from '../lib/cache.js';
import { logger } from '../lib/logger.js';
import { buildAzCommand } from '../lib/shell-escape.js';
export interface ServiceResult<T = unknown> {
success: boolean;
data?: T;
count?: number;
error?: string;
command?: string;
cached?: boolean;
}
export abstract class AzureService {
abstract readonly serviceName: string;
abstract readonly cliPrefix: string;
protected async execute(subCmd: string, options: Record<string, string> = {}): Promise<CommandResult> {
// Use secure shell escaping to prevent command injection
const fullSubCommand = `${this.cliPrefix} ${subCmd}`;
// Filter out empty values
const filteredOptions: Record<string, string> = {};
for (const [key, value] of Object.entries(options)) {
if (value) {
filteredOptions[key] = value;
}
}
// Build command with proper escaping
const cmd = buildAzCommand(fullSubCommand, filteredOptions);
logger.debug(`${this.serviceName} execute`, { service: this.serviceName, subCommand: subCmd });
return executeAzCommand(cmd, { enableRetry: true, showSummary: true });
}
protected toResult<T>(result: CommandResult, cmd?: string): ServiceResult<T> {
if (!result.success) {
return { success: false, error: result.stderr || 'Command failed', command: cmd };
}
const data = result.parsedOutput as T;
const count = Array.isArray(data) ? data.length : undefined;
return { success: true, data, count, command: cmd };
}
protected async cachedExecute<T>(
cacheKey: string,
operation: () => Promise<ServiceResult<T>>,
bypassCache = false
): Promise<ServiceResult<T>> {
if (bypassCache) return operation();
return withCache(cacheKey, operation);
}
abstract list(resourceGroup?: string): Promise<ServiceResult>;
}