< Summary - Envilder CLI

Information
Class: src/envilder/core/infrastructure/variableStore/FileVariableStore.ts
Assembly: Default
File(s): src/envilder/core/infrastructure/variableStore/FileVariableStore.ts
Tag: 299_25910610327
Line coverage
100%
Covered lines: 49
Uncovered lines: 0
Coverable lines: 49
Total lines: 130
Line coverage: 100%
Branch coverage
95%
Covered branches: 21
Total branches: 22
Branch coverage: 95.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

src/envilder/core/infrastructure/variableStore/FileVariableStore.ts

#LineLine coverage
 1import * as fs from 'node:fs/promises';
 2import * as dotenv from 'dotenv';
 3import { inject, injectable } from 'inversify';
 4import {
 5  DependencyMissingError,
 6  EnvironmentFileError,
 7} from '../../domain/errors/DomainErrors.js';
 8import type {
 9  MapFileConfig,
 10  ParsedMapFile,
 11} from '../../domain/MapFileConfig.js';
 12import type { ILogger } from '../../domain/ports/ILogger.js';
 13import type { IVariableStore } from '../../domain/ports/IVariableStore.js';
 14import { TYPES } from '../../types.js';
 15
 16@injectable()
 817export class FileVariableStore implements IVariableStore {
 18  private logger: ILogger;
 19
 20  constructor(@inject(TYPES.ILogger) logger: ILogger) {
 4621    if (!logger) {
 122      throw new DependencyMissingError('Logger must be specified');
 23    }
 4524    this.logger = logger;
 25  }
 26
 27  async getMapping(source: string): Promise<Record<string, string>> {
 928    const { mappings } = await this.getParsedMapping(source);
 629    return mappings;
 30  }
 31
 32  async getParsedMapping(source: string): Promise<ParsedMapFile> {
 1433    const raw = await this.readJsonFile(source);
 1134    const { $config, ...rest } = raw;
 35    const config: MapFileConfig =
 1136      $config && typeof $config === 'object' ? $config : {};
 1437    const mappings: Record<string, string> = {};
 1438    for (const [key, value] of Object.entries(rest)) {
 2139      if (!key.startsWith('$') && typeof value === 'string') {
 1640        mappings[key] = value;
 41      }
 42    }
 1143    return { config, mappings };
 44  }
 45
 46  private async readJsonFile(source: string): Promise<Record<string, unknown>> {
 1447    try {
 1448      const content = await fs.readFile(source, 'utf-8');
 1249      try {
 1250        return JSON.parse(content);
 51      } catch (_err: unknown) {
 152        this.logger.error(`Error parsing JSON from ${source}`);
 153        throw new EnvironmentFileError(
 54          `Invalid JSON in parameter map file: ${source}`,
 55        );
 56      }
 57    } catch (error) {
 358      if (error instanceof EnvironmentFileError) {
 159        throw error;
 60      }
 261      throw new EnvironmentFileError(`Failed to read map file: ${source}`);
 62    }
 63  }
 64
 65  async getEnvironment(source: string): Promise<Record<string, string>> {
 866    const envVariables: Record<string, string> = {};
 867    try {
 868      await fs.access(source);
 69    } catch {
 470      return envVariables;
 71    }
 472    const existingEnvContent = await fs.readFile(source, 'utf-8');
 273    const parsedEnv = dotenv.parse(existingEnvContent) || {};
 874    Object.assign(envVariables, parsedEnv);
 75
 876    return envVariables;
 77  }
 78
 79  async saveEnvironment(
 80    destination: string,
 81    envVariables: Record<string, string>,
 82  ): Promise<void> {
 1083    const envContent = Object.entries(envVariables)
 1084      .map(([key, value]) => `${key}=${this.escapeEnvValue(value)}`)
 85      .join('\n');
 86
 1087    try {
 1088      await fs.writeFile(destination, envContent);
 89    } catch (error) {
 90      const errorMessage =
 291        error instanceof Error ? error.message : String(error);
 292      this.logger.error(`Failed to write environment file: ${errorMessage}`);
 293      throw new EnvironmentFileError(
 94        `Failed to write environment file: ${errorMessage}`,
 95      );
 96    }
 97  }
 98
 99  private escapeEnvValue(value: string): string {
 100    // codeql[js/incomplete-sanitization]
 101    // CodeQL flags this as incomplete sanitization because we don't escape backslashes
 102    // before newlines. However, this is intentional: the dotenv library does NOT
 103    // interpret escape sequences (it treats \n literally as backslash+n, not as a newline).
 104    // Therefore, escaping backslashes would actually break the functionality by
 105    // doubling them when read back by dotenv. This is not a security issue in this context.
 10106    return value.replace(/(\r\n|\n|\r)/g, '\\n');
 107  }
 108}
 109
 110export async function readMapFileConfig(
 111  mapPath: string,
 112): Promise<MapFileConfig> {
 4113  try {
 4114    const content = await fs.readFile(mapPath, 'utf-8');
 3115    try {
 3116      const raw = JSON.parse(content);
 3117      const config = raw.$config;
 3118      return config && typeof config === 'object' ? config : {};
 119    } catch {
 1120      throw new EnvironmentFileError(
 121        `Invalid JSON in parameter map file: ${mapPath}`,
 122      );
 123    }
 124  } catch (error) {
 2125    if (error instanceof EnvironmentFileError) {
 1126      throw error;
 127    }
 1128    throw new EnvironmentFileError(`Failed to read map file: ${mapPath}`);
 129  }
 130}