Skip to content

miinhho/nestjs-saop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

nestjs-saop

npm version codecov Github Workflow package license

English | ν•œκ΅­μ–΄

Spring style AOP (Aspect Oriented Programming) in Nest.js

Features

  • βœ… Complete AOP Advice Types: Support for all 5 Spring-style AOP advice types

    • Around: Complete control over method execution (before, during, and after)
    • Before: Execute advice before method invocation
    • After: Execute advice after method completion (regardless of success/failure)
    • AfterReturning: Execute advice only when method completes successfully
    • AfterThrowing: Execute advice only when method throws an exception
  • βœ… Full TypeScript Support: Complete type safety with generics and interfaces

    • Strongly typed AOP contexts and options
    • Generic support for method return types and error types
    • IntelliSense support for all AOP operations
  • βœ… NestJS Integration: Seamless integration with NestJS module system

    • AOPModule.forRoot() for global AOP configuration
    • Automatic instance discovery using NestJS DiscoveryModule
    • Compatible with all NestJS dependency injection patterns
  • βœ… Flexible Configuration: Highly configurable AOP options and contexts

    • Conditional AOP execution based on runtime conditions
    • Multiple decorators per method with different configurations
  • βœ… Decorator Pattern Implementation: Clean decorator-based API

    • @Aspect({ order?: number }) decorator for AOP class identification with optional execution order control
    • Static method decorators for easy application

Installation

npm install nestjs-saop
# or
yarn add nestjs-saop
# or
pnpm add nestjs-saop

Quick Start

1. Import AOPModule

import { AOPModule } from 'nestjs-saop';

@Module({
  imports: [
    // ... other modules
    AOPModule.forRoot(),
  ],
})
export class AppModule {}

2. Create AOP Decorator Implementation

import { AOPDecorator, Aspect } from 'nestjs-saop';

@Aspect()
export class LoggingDecorator extends AOPDecorator {
  around({ method, proceed, options }) {
    return (...args: any[]) => {
      console.log('πŸ”„ Around: Before method call', ...args);
      const result = proceed(...args);
      console.log('πŸ”„ Around: After method call', result);
      return result;
    };
  }

  before({ method, options }) {
    return (...args: any[]) => {
      console.log('▢️ Before: Method called with', ...args);
    };
  }

  after({ method, options }) {
    return (...args: any[]) => {
      console.log('⏹️ After: Method completed');
    };
  }

  afterReturning({ method, options, result }) {
    return (...args: any[]) => {
      console.log('βœ… AfterReturning: Method returned', result);
    };
  }

  afterThrowing({ method, options, error }): (...args: any[]) => void {
    return (...args: any[]) => {
      console.log('❌ AfterThrowing: Method threw', error.message);
    };
  }
}

3. Register Decorator in Module

import { LoggingDecorator } from './logging.decorator';

@Module({
  providers: [LoggingDecorator],
})
export class AppModule {}

4. Use AOP Decorators

import { LoggingDecorator, CachingDecorator, PerformanceDecorator } from 'example-path';

@Injectable()
export class ExampleService {
  @LoggingDecorator.after({ level: 'info', logArgs: true, logResult: true })
  processData(data: any): string {
    return `Processed: ${data}`;
  }

  @CachingDecorator.afterReturn({ ttl: 300000 }) 
  async getUserById(id: string): Promise<User> {
    return await this.userRepository.findById(id);
  }

  @PerformanceDecorator.around({ logPerformance: true, threshold: 1000 })
  async expensiveOperation(): Promise<any> {
    await new Promise(resolve => setTimeout(resolve, 500));
    return { result: 'done' };
  }
}

Usage Guide

AOP execution cycle

  1. πŸ”„ Around
  2. ▢️ Before
  3. βœ… AfterReturning or ❌ AfterThrowing
  4. ⏹️ After
  5. πŸ”„ Around

AOP Execution Order

When multiple AOP decorators are applied to the same method, you can control the execution order using the order option in the @Aspect() decorator. Lower order values execute first. If no order is specified, the default is Number.MAX_SAFE_INTEGER, giving it the lowest priority.

import { AOPDecorator, Aspect } from 'nestjs-saop';

class AOPTracker {
  static executionOrder: string[] = [];

  static reset() {
    this.executionOrder = [];
  }
}

@Aspect({ order: 1 })
class FirstAOP extends AOPDecorator {
  before() {
    return () => {
      AOPTracker.executionOrder.push('First');
    };
  }
}

@Aspect({ order: 2 })
class SecondAOP extends AOPDecorator {
  before() {
    return () => {
      AOPTracker.executionOrder.push('Second');
    };
  }
}

@Aspect({ order: 3 })
class ThirdAOP extends AOPDecorator {
  before() {
    return () => {
      AOPTracker.executionOrder.push('Third');
    };
  }
}

@Injectable()
class TestService {
  @FirstAOP.before()
  @SecondAOP.before()
  @ThirdAOP.before()
  getOrdered(): string {
    return 'Ordered AOP executed';
  }
}

In this example, when getOrdered() is called, the AOPs will execute in order: First (order 1), Second (order 2), Third (order 3).

AOP Advice Types

Around Advice

Use case: Complete control over method execution, perfect for caching, performance monitoring, or transaction management.

@Aspect()
export class CachingDecorator extends AOPDecorator {
  private cache = new Map();

  around({ method, options, proceed }) {
    return (...args: any[]) => {
      const key = `${method.name}:${JSON.stringify(args)}`;

      if (this.cache.has(key)) {
        console.log('πŸ”„ Cache hit!');
        return this.cache.get(key);
      }

      console.log('πŸ”„ Cache miss, executing method...');
      const result = proceed(...args);

      if (options.ttl) {
        setTimeout(() => this.cache.delete(key), options.ttl);
      }

      this.cache.set(key, result);
      return result;
    };
  }
}

// Usage
@Injectable()
export class UserService {
  @CachingDecorator.around({ ttl: 300000 })
  async getUserById(id: string): Promise<User> {
    return await this.userRepository.findById(id);
  }
}

Before Advice

Use case: Logging method calls, validation, authentication checks.

@Aspect()
export class LoggingDecorator extends AOPDecorator {
  before({ method, options }) {
    return (...args: any[]) => {
      console.log(`▢️ [${new Date().toISOString()}] ${method.name} called with:`, args);
    };
  }
}

// Usage
@Injectable()
export class PaymentService {
  @LoggingDecorator.before({ level: 'info' })
  async processPayment(amount: number, userId: string): Promise<PaymentResult> {
    return { success: true, transactionId: 'tx_123' };
  }
}

After Advice

Use case: Cleanup operations, resource management, regardless of method success/failure.

@Aspect()
export class ResourceCleanupDecorator extends AOPDecorator {
  after({ method, options }) {
    return (...args: any[]) => {
      console.log('🧹 Cleaning up resources after method execution');
      // Cleanup logic here
    };
  }
}

// Usage
@Injectable()
export class FileService {
  @ResourceCleanupDecorator.after()
  async processFile(filePath: string): Promise<void> {
    const fileHandle = await fs.open(filePath, 'r');
    try {
      await this.processFileContent(fileHandle);
    } finally {
      await fileHandle.close();
    }
  }
}

AfterReturning Advice

Use case: Post-processing successful results, response formatting, metrics collection.

@Aspect()
export class ResponseFormatterDecorator extends AOPDecorator {
  afterReturning({ method, options, result }) {
    return (...args: any[]) => {
      console.log('βœ… Method completed successfully');
      if (options.format === 'json') {
        return {
          success: true,
          data: result,
          timestamp: new Date().toISOString()
        };
      }
      return result;
    };
  }
}

// Usage
@Injectable()
export class ApiService {
  @ResponseFormatterDecorator.afterReturning({ format: 'json' })
  async getUserData(userId: string): Promise<UserData> {
    return await this.userRepository.findById(userId);
  }
}

AfterThrowing Advice

Use case: Error logging, error recovery, fallback mechanisms.

@Aspect()
export class ErrorHandlingDecorator extends AOPDecorator {
  constructor(private readonly errorLogger: ErrorLogger) {}

  afterThrowing({ method, options, error }) {
    return (...args: any[]) => {
      console.error(`❌ Method ${method.name} failed:`, error.message);

      if (options.retry && options.retryCount < 3) {
        console.log(`πŸ”„ Retrying... (${options.retryCount + 1}/3)`);
        // Implement retry logic
      }

      // Log to external service
      this.errorLogger.log({
        method: method.name,
        error: error.message,
        timestamp: new Date().toISOString(),
        args: options.logArgs ? args : undefined
      });
    };
  }
}

// Usage
@Injectable()
export class ExternalApiService {
  @ErrorHandlingDecorator.afterThrowing({ retry: true, retryCount: 0, logArgs: true })
  async callExternalAPI(endpoint: string): Promise<ExternalData> {
    const response = await fetch(endpoint);
    if (!response.ok) {
      throw new Error(`API call failed: ${response.status}`);
    }
    return response.json();
  }
}

Configuration Options

AOPDecorator Generics

The AOPDecorator class uses TypeScript generics to provide strong typing and better IntelliSense support:

Usage Examples:

// Basic usage with default generics
@Aspect()
export class BasicDecorator extends AOPDecorator {
  // Options = AOPOptions (default type)
}

// With custom options type
interface LoggingOptions {
  level: 'debug' | 'info' | 'warn' | 'error';
  includeTimestamp: boolean;
}

@Aspect()
export class LoggingDecorator extends AOPDecorator {
  // Generic type parameter for custom options
  // This enables TypeScript to infer the option type when using LoggingDecorator.before()
  before({ method, options }: UnitAOPContext<LoggingOptions>) {
    return (...args: any[]) => {
      const timestamp = options.includeTimestamp ? `[${new Date().toISOString()}] ` : '';
      console.log(`${timestamp}${options.level.toUpperCase()}: ${method.name} called`);
    };
  }
}

// With return type and error type
interface ApiResponse<T> {
  success: boolean;
  data: T;
  error?: string;
}

@Aspect()
export class ApiDecorator extends AOPDecorator {
  // `AOPOptions` here is the basic option type.
  afterReturning({ method, options, result }: ResultAOPContext<AOPOptions, ApiResponse<any>>) {
    return (...args: any[]) => {
      console.log(`βœ… API call successful: ${method.name}`);
      // result is typed as ApiResponse<any>
      if (result.success) {
        console.log(`πŸ“Š Response data:`, result.data);
      }
    };
  }

  // `AOPOptions` here is the basic option type.
  afterThrowing({ method, options, error }: ErrorAOPContext<AOPOptions, Error>) {
    return (...args: any[]) => {
      console.error(`❌ API call failed: ${method.name}`, error.message);
      // error is typed as Error
    };
  }
}

// Usage with typed decorators
@Injectable()
export class UserService {
  @LoggingDecorator.before({
    level: 'info',
    includeTimestamp: true
  })
  async getUser(id: string): Promise<User> {
    // Method implementation
  }

  @ApiDecorator.afterReturning()
  async getUserData(id: string): Promise<ApiResponse<User>> {
    // Method implementation
  }
}

Benefits of Using Generics:

  1. Type Safety: Catch type errors at compile time
  2. Better IntelliSense: IDE provides accurate autocompletion
  3. Self-Documenting Code: Types serve as documentation

Context Types by Advice Type:

// Before, After advice
UnitAOPContext<Options> = {
  method: Function;
  options: Options;
}

// AfterReturning advice
ResultAOPContext<Options, ReturnType> = {
  method: Function;
  options: Options;
  result: ReturnType;  // Available only in afterReturning
}

// Around advice
AroundAOPContext<Options> = {
  method: Function;
  instance: object;
  proceed: Function;
  options: Options;
};

// AfterThrowing advice
ErrorAOPContext<Options, ErrorType> = {
  method: Function;
  options: Options;
  error: ErrorType;   // Available only in afterThrowing
}

Multiple Decorators on Single Method

@Injectable()
export class ComplexService {
  @LoggingDecorator.before({ level: 'info', logArgs: true })
  @PerformanceDecorator.around({ threshold: 1000, logPerformance: true })
  @CachingDecorator.around({ ttl: 300000 })
  @ErrorHandlingDecorator.afterThrowing({ retry: true, logArgs: true })
  async complexOperation(data: ComplexData): Promise<ComplexResult> {
    // Method will be enhanced with:
    // 1. Performance monitoring around execution
    // 2. Logging before execution
    // 3. Error handling if something goes wrong
    // 4. Caching around execution
    return await this.processComplexData(data);
  }
}

Importing AOPModule

The AOPModule.forRoot method configures the AOPModule as a global module. However, you can also import the AOPModule into specific modules if needed.

@Module({
  imports: [AOPModule],
})
export class SpecificModule {}

Testing AOP Decorators

When testing with NestJS's TestingModule, ensure that you call the init() method to properly initialize the AOP system.

describe('AOP Integration (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AOPModule.forRoot()],
      providers: [LoggingDecorator, TestService],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init(); // Required for AOP initialization
  });

  it('should apply AOP advice to service methods', () => {
    const testService = app.get(TestService);

    const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

    const result = testService.testMethod('test');

    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('Before: Method called')
    );
    expect(result).toBe('processed: test');
  });
});

Contributing

We welcome contributions! Please see our Contributing Guide for details.

About

Spring style AOP (Aspect Oriented Programming) in Nest.js

Resources

License

Contributing

Stars

Watchers

Forks

Contributors 2

  •  
  •