Files
npmextra/readme.plan.md

6.0 KiB

AppData Refactoring Plan

Overview

Refactor the AppData class to improve elegance, maintainability, and extensibility while maintaining 100% backward compatibility.

Current Issues

  • 100+ lines of nested switch statements in processEnvMapping
  • Static helpers recreate Qenv instances on every call
  • Complex boolean conversion logic scattered across multiple places
  • Typo: "ephermal" should be "ephemeral"
  • Difficult to test and extend with new transformations

Architecture Improvements

1. Singleton Qenv Provider

Create a shared Qenv instance to avoid repeated instantiation:

let sharedQenv: plugins.qenv.Qenv | undefined;

function getQenv(): plugins.qenv.Qenv {
  if (!sharedQenv) {
    sharedQenv = new plugins.qenv.Qenv(
      process.cwd(),
      plugins.path.join(process.cwd(), '.nogit')
    );
  }
  return sharedQenv;
}

2. Centralized Type Converters

Extract all conversion logic into pure utility functions:

function toBoolean(value: unknown): boolean {
  if (typeof value === 'boolean') return value;
  if (value == null) return false;
  const s = String(value).toLowerCase();
  return s === 'true';
}

function toJson<T>(value: unknown): T | undefined {
  if (typeof value === 'string') {
    try {
      return JSON.parse(value);
    } catch {
      return undefined;
    }
  }
  return value as T;
}

function fromBase64(value: unknown): string {
  if (value == null) return '';
  return Buffer.from(String(value), 'base64').toString('utf8');
}

function toNumber(value: unknown): number | undefined {
  if (value == null) return undefined;
  const num = Number(value);
  return Number.isNaN(num) ? undefined : num;
}

function toString(value: unknown): string | undefined {
  if (value == null) return undefined;
  return String(value);
}

3. Declarative Pipeline Architecture

Replace the giant switch statement with a composable pipeline:

Data Structures

type MappingSpec = {
  source: 
    | { type: 'env', key: string }
    | { type: 'hard', value: string };
  transforms: Transform[];
}

type Transform = 'boolean' | 'json' | 'base64' | 'number';

Pipeline Functions

// Parse mapping string into spec
function parseMappingSpec(input: string): MappingSpec

// Resolve the source value
async function resolveSource(source: MappingSpec['source']): Promise<unknown>

// Apply transformations
function applyTransforms(value: unknown, transforms: Transform[]): unknown

// Complete pipeline
async function processMappingValue(mappingString: string): Promise<unknown>

4. Transform Registry

Enable easy extension with new transforms:

const transformRegistry: Record<string, (v: unknown) => unknown> = {
  boolean: toBoolean,
  json: toJson,
  base64: fromBase64,
  number: toNumber,
};

5. Simplified processEnvMapping

Build pure object tree first, then write to kvStore:

async function evaluateMappingValue(mappingValue: any): Promise<any> {
  if (typeof mappingValue === 'string') {
    return processMappingValue(mappingValue);
  }
  if (mappingValue && typeof mappingValue === 'object') {
    const out: any = {};
    for (const [k, v] of Object.entries(mappingValue)) {
      out[k] = await evaluateMappingValue(v);
    }
    return out;
  }
  return undefined;
}

// Main loop becomes:
for (const key in this.options.envMapping) {
  const evaluated = await evaluateMappingValue(this.options.envMapping[key]);
  if (evaluated !== undefined) {
    await this.kvStore.writeKey(key as keyof T, evaluated);
  }
}

Backward Compatibility

Supported Prefixes (Maintained)

  • hard: - Hardcoded value
  • hard_boolean: - Hardcoded boolean
  • hard_json: - Hardcoded JSON
  • hard_base64: - Hardcoded base64
  • boolean: - Environment variable as boolean
  • json: - Environment variable as JSON
  • base64: - Environment variable as base64

Supported Suffixes (Maintained)

  • _JSON - Auto-parse as JSON
  • _BASE64 - Auto-decode from base64

Typo Fix Strategy

  • Add ephemeral option to interface
  • Keep reading ephermal for backward compatibility
  • Log deprecation warning when old spelling is used

Implementation Steps

  1. Add utility functions at the top of the file
  2. Implement pipeline functions (parseMappingSpec, resolveSource, applyTransforms)
  3. Refactor processEnvMapping to use the pipeline
  4. Update static helpers to use shared utilities
  5. Fix typo with compatibility shim
  6. Add error boundaries for better error reporting
  7. Test to ensure backward compatibility

Benefits

Code Quality

  • 70% reduction in processEnvMapping complexity
  • Better separation of concerns
  • Easier testing - each function is pure and testable
  • Cleaner error handling with boundaries

Performance

  • Shared Qenv instance reduces allocations
  • Optional parallelization with Promise.all
  • Fewer repeated operations

Maintainability

  • Extensible - Easy to add new transforms
  • Readable - Clear pipeline flow
  • Debuggable - Each step can be logged
  • Type-safe - Better TypeScript support

Testing Strategy

  1. Unit tests for each utility function
  2. Integration tests for the full pipeline
  3. Backward compatibility tests for all existing prefixes/suffixes
  4. Edge case tests for error conditions

Future Extensions

With the transform registry, adding new features becomes trivial:

// Add YAML support
transformRegistry['yaml'] = (v) => YAML.parse(String(v));

// Add integer parsing
transformRegistry['int'] = (v) => parseInt(String(v), 10);

// Add custom transformers
transformRegistry['uppercase'] = (v) => String(v).toUpperCase();

Migration Path

  1. Implement new architecture alongside existing code
  2. Gradually migrate internal usage
  3. Mark old patterns as deprecated (with warnings)
  4. Remove deprecated code in next major version

Success Metrics

  • All existing tests pass
  • No breaking changes for users
  • Reduced code complexity (measurable via cyclomatic complexity)
  • Improved test coverage
  • Better performance (fewer allocations, optional parallelization)