Registration

Goal: Create a provider and register it in a certain scope.

Registrations connect providers to the container with keys and configuration. They can be created using decorators or the fluent API, providing flexibility in how you organize your dependency configuration.

The following diagram shows how registrations are processed and applied to containers:

flowchart TD Start([Developer creates Registration]) --> Config[Configure Registration:
bindToKey, pipe, when] Config --> CreateProvider[Registration creates provider with configuration] CreateProvider --> AddToContainer[Developer addRegistration to container] AddToContainer --> Apply[Container applies Registration] Apply --> CheckScope{Scope matches?} CheckScope -->|Yes| Register[Register provider & alias] CheckScope -->|No| Skip[Skip registration] Register --> Complete([Registration complete]) Skip --> Complete classDef startEnd fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff classDef process fill:#6C5CE7,stroke:#4834D4,stroke-width:2px,color:#fff classDef config fill:#00B894,stroke:#00A085,stroke-width:2px,color:#fff classDef decision fill:#FDCB6E,stroke:#E17055,stroke-width:2px,color:#2D3436 classDef success fill:#00B894,stroke:#00A085,stroke-width:2px,color:#fff classDef skip fill:#636E72,stroke:#2D3436,stroke-width:2px,color:#fff class Start,Complete startEnd class CreateProvider,AddToContainer,Apply process class Config config class CheckScope decision class Register success class Skip skip

There are several ways to create and apply registrations:

The @register decorator provides a declarative way to register classes. This is the most convenient approach for most use cases.

TypeScript __tests__/readme/registration.spec.ts
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});

The fluent API provides more control and is useful when you need dynamic registration or want to avoid decorators.

TypeScript __tests__/readme/registration.spec.ts
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});

Keys identify dependencies in the container. They can be strings, symbols, or tokens. Understanding how keys work is essential for effective dependency management.

TypeScript __tests__/readme/registration.spec.ts
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});

Scope match rules (ScopeMatchRule) control which scopes a provider is available in. This allows you to register different implementations for different environments or contexts.

TypeScript __tests__/readme/registration.spec.ts
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});
import {
  bindTo,
  Container,
  DependencyMissingKeyError,
  register,
  Registration as R,
  scope,
  select as s,
  singleton,
} from 'ts-ioc-container';

/**
 * User Management Domain - Registration Patterns
 *
 * Registrations define how dependencies are bound to the container.
 * Common patterns:
 * - Register by class (auto-generates key from class name)
 * - Register by value (constants, configuration)
 * - Register by factory function (dynamic creation)
 * - Register with aliases (multiple keys for same service)
 *
 * This is the foundation for dependency injection - telling the container
 * "when someone asks for X, give them Y".
 */
describe('Registration module', function () {
  const createAppContainer = () => new Container({ tags: ['application'] });

  it('should register class with scope and lifecycle', function () {
    // Logger is registered at application scope as a singleton
    @register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });

  it('should register configuration value', function () {
    // Register application configuration as a value
    const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));

    expect(appContainer.resolve('Environment')).toBe('production');
  });

  it('should register factory function', function () {
    // Factory functions are useful for dynamic creation
    const appContainer = createAppContainer().addRegistration(
      R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
    );

    expect(appContainer.resolve('RequestId')).toContain('app-');
  });

  it('should raise an error if binding key is not provided', () => {
    // Values and functions must have explicit keys (classes use class name by default)
    expect(() => {
      createAppContainer().addRegistration(R.fromValue('orphan-value'));
    }).toThrowError(DependencyMissingKeyError);
  });

  it('should register dependency by class name when no key decorator is used', function () {
    // Without @register(bindTo('key')), the class name becomes the key
    class FileLogger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));

    expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
  });

  it('should register with multiple keys using aliases', function () {
    // Same service accessible via direct key and alias
    @register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
    class Logger {}

    const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));

    // Accessible via alias (for group resolution)
    expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
    // Accessible via direct key
    expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
  });
});

Registrations support the pipeline pattern, allowing you to chain multiple transformations:

TypeScript
import { args, singleton, scopeAccess } from 'ts-ioc-container';

const container = new Container()
.addRegistration(
  R.fromClass(ConfigService)
    .bindToKey('IConfig')
    .when((s) => s.hasTag('root'))
    .pipe(args('/config.json'))
    .pipe(singleton())
    .pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')))
);
import { args, singleton, scopeAccess } from 'ts-ioc-container';

const container = new Container()
.addRegistration(
  R.fromClass(ConfigService)
    .bindToKey('IConfig')
    .when((s) => s.hasTag('root'))
    .pipe(args('/config.json'))
    .pipe(singleton())
    .pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')))
);

Container modules encapsulate registration logic, making it easy to organize and compose your dependency configuration.

TypeScript __tests__/readme/containerModule.spec.ts
import 'reflect-metadata';
import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';

/**
 * User Management Domain - Container Modules
 *
 * Modules organize related registrations and allow swapping implementations
 * based on environment (development, testing, production).
 *
 * Common module patterns:
 * - ProductionModule: Real database, external APIs, email service
 * - DevelopmentModule: In-memory database, mock APIs, console logging
 * - TestingModule: Mocks with assertion capabilities
 *
 * This enables:
 * - Easy environment switching
 * - Isolated testing without external dependencies
 * - Feature flags via module composition
 */

// Auth service interface - same API for all environments
interface IAuthService {
  authenticate(email: string, password: string): boolean;
  getServiceType(): string;
}

// Production: Real authentication with database lookup
@register(bindTo('IAuthService'))
class ProductionAuthService implements IAuthService {
  authenticate(email: string, password: string): boolean {
    // In production, this would query the database
    return email === 'admin@example.com' && password === 'secure_password';
  }

  getServiceType(): string {
    return 'production';
  }
}

// Development: Accepts any credentials for easy testing
@register(bindTo('IAuthService'))
class DevelopmentAuthService implements IAuthService {
  authenticate(_email: string, _password: string): boolean {
    // Always succeed in development for easier testing
    return true;
  }

  getServiceType(): string {
    return 'development';
  }
}

// Production module - real services with security
class ProductionModule implements IContainerModule {
  applyTo(container: IContainer): void {
    container.addRegistration(R.fromClass(ProductionAuthService));
    // In a real app, also register:
    // - Real database connection
    // - External email service
    // - Payment gateway
  }
}

// Development module - mocks and conveniences
class DevelopmentModule implements IContainerModule {
  applyTo(container: IContainer): void {
    container.addRegistration(R.fromClass(DevelopmentAuthService));
    // In a real app, also register:
    // - In-memory database
    // - Console email logger
    // - Mock payment gateway
  }
}

describe('Container Modules', function () {
  function createContainer(isProduction: boolean) {
    const module = isProduction ? new ProductionModule() : new DevelopmentModule();
    return new Container().useModule(module);
  }

  it('should use production auth with strict validation', function () {
    const container = createContainer(true);
    const auth = container.resolve<IAuthService>('IAuthService');

    expect(auth.getServiceType()).toBe('production');
    expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
    expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
  });

  it('should use development auth with permissive validation', function () {
    const container = createContainer(false);
    const auth = container.resolve<IAuthService>('IAuthService');

    expect(auth.getServiceType()).toBe('development');
    // Development mode accepts any credentials
    expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
  });

  it('should allow composing multiple modules', function () {
    // Modules can be composed for feature flags or A/B testing
    class FeatureFlagModule implements IContainerModule {
      constructor(private enableNewFeature: boolean) {}

      applyTo(container: IContainer): void {
        if (this.enableNewFeature) {
          // Register new feature implementations
        }
      }
    }

    const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));

    // Base services from ProductionModule
    expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
  });
});
import 'reflect-metadata';
import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';

/**
 * User Management Domain - Container Modules
 *
 * Modules organize related registrations and allow swapping implementations
 * based on environment (development, testing, production).
 *
 * Common module patterns:
 * - ProductionModule: Real database, external APIs, email service
 * - DevelopmentModule: In-memory database, mock APIs, console logging
 * - TestingModule: Mocks with assertion capabilities
 *
 * This enables:
 * - Easy environment switching
 * - Isolated testing without external dependencies
 * - Feature flags via module composition
 */

// Auth service interface - same API for all environments
interface IAuthService {
  authenticate(email: string, password: string): boolean;
  getServiceType(): string;
}

// Production: Real authentication with database lookup
@register(bindTo('IAuthService'))
class ProductionAuthService implements IAuthService {
  authenticate(email: string, password: string): boolean {
    // In production, this would query the database
    return email === 'admin@example.com' && password === 'secure_password';
  }

  getServiceType(): string {
    return 'production';
  }
}

// Development: Accepts any credentials for easy testing
@register(bindTo('IAuthService'))
class DevelopmentAuthService implements IAuthService {
  authenticate(_email: string, _password: string): boolean {
    // Always succeed in development for easier testing
    return true;
  }

  getServiceType(): string {
    return 'development';
  }
}

// Production module - real services with security
class ProductionModule implements IContainerModule {
  applyTo(container: IContainer): void {
    container.addRegistration(R.fromClass(ProductionAuthService));
    // In a real app, also register:
    // - Real database connection
    // - External email service
    // - Payment gateway
  }
}

// Development module - mocks and conveniences
class DevelopmentModule implements IContainerModule {
  applyTo(container: IContainer): void {
    container.addRegistration(R.fromClass(DevelopmentAuthService));
    // In a real app, also register:
    // - In-memory database
    // - Console email logger
    // - Mock payment gateway
  }
}

describe('Container Modules', function () {
  function createContainer(isProduction: boolean) {
    const module = isProduction ? new ProductionModule() : new DevelopmentModule();
    return new Container().useModule(module);
  }

  it('should use production auth with strict validation', function () {
    const container = createContainer(true);
    const auth = container.resolve<IAuthService>('IAuthService');

    expect(auth.getServiceType()).toBe('production');
    expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
    expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
  });

  it('should use development auth with permissive validation', function () {
    const container = createContainer(false);
    const auth = container.resolve<IAuthService>('IAuthService');

    expect(auth.getServiceType()).toBe('development');
    // Development mode accepts any credentials
    expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
  });

  it('should allow composing multiple modules', function () {
    // Modules can be composed for feature flags or A/B testing
    class FeatureFlagModule implements IContainerModule {
      constructor(private enableNewFeature: boolean) {}

      applyTo(container: IContainer): void {
        if (this.enableNewFeature) {
          // Register new feature implementations
        }
      }
    }

    const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));

    // Base services from ProductionModule
    expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
  });
});
  1. Creation: Registration is created with a provider
  2. Configuration: Keys, scopes, and features are configured
  3. Application: Registration is applied to container via addRegistration()
  4. Validation: Scope match rules are evaluated
  5. Storage: Provider is stored in container’s provider map
  6. Alias Registration: Aliases are registered if provided