import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { from, Observable, switchMap } from 'rxjs'; import { MediaStorageService } from '../media/media-storage.service'; @Injectable() export class MediaUrlInterceptor implements NestInterceptor { constructor(private readonly mediaStorageService: MediaStorageService) {} intercept(context: ExecutionContext, next: CallHandler): Observable { if (context.getType() !== 'http' || !this.mediaStorageService.shouldSignResponseUrls()) { return next.handle(); } return next .handle() .pipe(switchMap((data) => from(this.resolveValue(data, new WeakMap())))); } private async resolveValue(value: unknown, seen: WeakMap): Promise { if (typeof value === 'string') { return this.mediaStorageService.resolveResponseUrl(value); } if ( value === null || typeof value !== 'object' || value instanceof Date || Buffer.isBuffer(value) ) { return value; } const existing = seen.get(value); if (existing) { return existing; } if (Array.isArray(value)) { const result: unknown[] = []; seen.set(value, result); const entries = await Promise.all(value.map((entry) => this.resolveValue(entry, seen))); result.push(...entries); return result; } const serializable = typeof (value as { toJSON?: unknown }).toJSON === 'function' ? (value as { toJSON: () => unknown }).toJSON() : value; if (serializable !== value) { return this.resolveValue(serializable, seen); } const result: Record = {}; seen.set(value, result); await Promise.all( Object.entries(value).map(async ([key, entryValue]) => { result[key] = await this.resolveValue(entryValue, seen); }), ); return result; } }