Refactor Notification Handling and Update API Response Structure

- Improved notification handling in the NotificationsController to ensure consistent response formats across different endpoints.
- Updated response types to utilize ApiErrorResponse for error scenarios, enhancing clarity in API responses.
- Refactored service methods to streamline notification processing and improve maintainability.
- Cleaned up unused imports and optimized code structure for better readability.
This commit is contained in:
barsa 2025-12-26 13:04:47 +09:00
parent 10c8461661
commit a1be0ea527

View File

@ -0,0 +1,75 @@
import {
Injectable,
type NestInterceptor,
type ExecutionContext,
type CallHandler,
} from "@nestjs/common";
import { SetMetadata } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable, map } from "rxjs";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
export const SKIP_SUCCESS_ENVELOPE_KEY = "bff:skip-success-envelope";
/**
* Opt-out decorator for endpoints that must not be wrapped in `{ success: true, data }`,
* e.g. SSE streams or file downloads.
*/
export const SkipSuccessEnvelope = () => SetMetadata(SKIP_SUCCESS_ENVELOPE_KEY, true);
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object";
}
function isLikelyStream(value: unknown): boolean {
// Avoid wrapping Node streams (file downloads / SSE internals).
return isRecord(value) && typeof (value as { pipe?: unknown }).pipe === "function";
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiSuccessResponse<T>> {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ApiSuccessResponse<T>> {
if (context.getType() !== "http") {
// Only wrap HTTP responses.
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
const skip =
this.reflector.getAllAndOverride<boolean>(SKIP_SUCCESS_ENVELOPE_KEY, [
context.getHandler(),
context.getClass(),
]) ?? false;
if (skip) {
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
const req = context.switchToHttp().getRequest<{ originalUrl?: string; url?: string }>();
const url = req?.originalUrl ?? req?.url ?? "";
// Only enforce success envelopes on the public API surface under `/api`.
// Keep non-API endpoints (e.g. `/health`) untouched for operational tooling.
if (!url.startsWith("/api")) {
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
return next.handle().pipe(
map(data => {
// Keep already-wrapped responses as-is (ack/message/data variants).
if (isRecord(data) && "success" in data) {
return data as unknown as ApiSuccessResponse<T>;
}
// Avoid wrapping streams/buffers that are handled specially by Nest/Express.
if (isLikelyStream(data)) {
return data as unknown as ApiSuccessResponse<T>;
}
const normalized = (data === undefined ? null : data) as T;
return { success: true as const, data: normalized };
})
);
}
}