< Summary - Envilder Core (TypeScript)

Information
Class: src/envilder/core/infrastructure/variableStore/FileVariableStore.ts
Assembly: Default
File(s): src/envilder/core/infrastructure/variableStore/FileVariableStore.ts
Tag: 151_24479375065
Line coverage
100%
Covered lines: 45
Uncovered lines: 0
Coverable lines: 45
Total lines: 124
Line coverage: 100%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.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) {
 4421    if (!logger) {
 122      throw new DependencyMissingError('Logger must be specified');
 23    }
 4324    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> {
 1233    const raw = await this.readJsonFile(source);
 934    const { $config, ...rest } = raw;
 35    const config: MapFileConfig =
 936      $config && typeof $config === 'object' ? $config : {};
 1237    return { config, mappings: rest as Record<string, string> };
 38  }
 39
 40  private async readJsonFile(source: string): Promise<Record<string, unknown>> {
 1241    try {
 1242      const content = await fs.readFile(source, 'utf-8');
 1043      try {
 1044        return JSON.parse(content);
 45      } catch (_err: unknown) {
 146        this.logger.error(`Error parsing JSON from ${source}`);
 147        throw new EnvironmentFileError(
 48          `Invalid JSON in parameter map file: ${source}`,
 49        );
 50      }
 51    } catch (error) {
 352      if (error instanceof EnvironmentFileError) {
 153        throw error;
 54      }
 255      throw new EnvironmentFileError(`Failed to read map file: ${source}`);
 56    }
 57  }
 58
 59  async getEnvironment(source: string): Promise<Record<string, string>> {
 860    const envVariables: Record<string, string> = {};
 861    try {
 862      await fs.access(source);
 63    } catch {
 464      return envVariables;
 65    }
 466    const existingEnvContent = await fs.readFile(source, 'utf-8');
 267    const parsedEnv = dotenv.parse(existingEnvContent) || {};
 868    Object.assign(envVariables, parsedEnv);
 69
 870    return envVariables;
 71  }
 72
 73  async saveEnvironment(
 74    destination: string,
 75    envVariables: Record<string, string>,
 76  ): Promise<void> {
 1077    const envContent = Object.entries(envVariables)
 1078      .map(([key, value]) => `${key}=${this.escapeEnvValue(value)}`)
 79      .join('\n');
 80
 1081    try {
 1082      await fs.writeFile(destination, envContent);
 83    } catch (error) {
 84      const errorMessage =
 285        error instanceof Error ? error.message : String(error);
 286      this.logger.error(`Failed to write environment file: ${errorMessage}`);
 287      throw new EnvironmentFileError(
 88        `Failed to write environment file: ${errorMessage}`,
 89      );
 90    }
 91  }
 92
 93  private escapeEnvValue(value: string): string {
 94    // codeql[js/incomplete-sanitization]
 95    // CodeQL flags this as incomplete sanitization because we don't escape backslashes
 96    // before newlines. However, this is intentional: the dotenv library does NOT
 97    // interpret escape sequences (it treats \n literally as backslash+n, not as a newline).
 98    // Therefore, escaping backslashes would actually break the functionality by
 99    // doubling them when read back by dotenv. This is not a security issue in this context.
 10100    return value.replace(/(\r\n|\n|\r)/g, '\\n');
 101  }
 102}
 103
 104export async function readMapFileConfig(
 105  mapPath: string,
 106): Promise<MapFileConfig> {
 4107  try {
 4108    const content = await fs.readFile(mapPath, 'utf-8');
 3109    try {
 3110      const raw = JSON.parse(content);
 3111      const config = raw.$config;
 3112      return config && typeof config === 'object' ? config : {};
 113    } catch {
 1114      throw new EnvironmentFileError(
 115        `Invalid JSON in parameter map file: ${mapPath}`,
 116      );
 117    }
 118  } catch (error) {
 2119    if (error instanceof EnvironmentFileError) {
 1120      throw error;
 121    }
 1122    throw new EnvironmentFileError(`Failed to read map file: ${mapPath}`);
 123  }
 124}