All files / src/common/interceptors etag-cache.interceptor.ts

96.77% Statements 30/31
100% Branches 13/13
80% Functions 4/5
96.55% Lines 28/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 851x             1x 1x   1x 1x   1x 1x                 1x   15x 15x       14x 14x     14x 3x       11x 1x       10x         10x           10x     10x 3x       7x     7x 6x               1x   1x 1x     1x      
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Inject,
} from '@nestjs/common';
import { Observable, NEVER } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Response, Request } from 'express';
import { ETagCacheService } from '../cache/etag-cache.service';
import { Reflector } from '@nestjs/core';
 
export const CACHE_TTL_KEY = 'cache_ttl';
export const CACHE_KEY_KEY = 'cache_key';
 
/**
 * Interceptor that caches GET responses with ETag support.
 * Works in conjunction with CacheMiddleware:
 * - Middleware checks cache BEFORE handler and returns early if cached
 * - Interceptor caches responses AFTER handler executes (for new data)
 */
@Injectable()
export class ETagCacheInterceptor implements NestInterceptor {
  constructor(
    @Inject(ETagCacheService) private cacheService: ETagCacheService,
    private reflector: Reflector,
  ) {}
 
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest<Request>();
    const res = context.switchToHttp().getResponse<Response>();
 
    // Only cache GET requests
    if (req.method !== 'GET') {
      return next.handle();
    }
 
    // Check if middleware already sent a cached response
    if (res.headersSent) {
      return NEVER;
    }
 
    // Get cache key from response locals (set by middleware) or generate
    const cacheKey = res.locals.cacheKey || req.url;
 
    // Get custom TTL from decorator (default 30s)
    // Note: TTL is retrieved for future use but cache service handles TTL internally
    const _ttl =
      this.reflector.getAllAndOverride<number>(CACHE_TTL_KEY, [
        context.getHandler(),
        context.getClass(),
      ]) ?? 30 * 1000;
 
    // Execute handler and cache result
    return next.handle().pipe(
      tap((data) => {
        // Skip if headers already sent or no data
        if (res.headersSent || data === undefined || data === null) {
          return;
        }
 
        // Cache the response
        const etag = this.cacheService.set(cacheKey, data);
 
        // Set ETag header if caching succeeded and headers not sent
        if (etag && !res.headersSent) {
          res.set('ETag', etag);
        }
      }),
    );
  }
}
 
// Decorators for custom cache configuration
import { SetMetadata } from '@nestjs/common';
 
export function CacheTTL(ttlMs: number) {
  return SetMetadata(CACHE_TTL_KEY, ttlMs);
}
 
export function CacheKey(key: string) {
  return SetMetadata(CACHE_KEY_KEY, key);
}