Picsur/backend/src/layers/success/success.interceptor.ts

119 lines
3.1 KiB
TypeScript
Raw Normal View History

2022-02-24 20:32:31 +00:00
import {
2022-03-28 10:50:39 +00:00
CallHandler,
ExecutionContext,
2022-08-26 18:40:16 +00:00
Injectable,
Logger,
2022-04-04 08:36:59 +00:00
NestInterceptor,
2022-12-27 15:05:53 +00:00
Optional,
2022-02-24 20:32:31 +00:00
} from '@nestjs/common';
2022-04-04 08:36:59 +00:00
import { Reflector } from '@nestjs/core';
2022-08-26 18:40:16 +00:00
import { FastifyReply } from 'fastify';
2022-04-05 18:37:25 +00:00
import { ApiAnySuccessResponse } from 'picsur-shared/dist/dto/api/api.dto';
import { Fail, FT } from 'picsur-shared/dist/types/failable';
2022-04-04 08:36:59 +00:00
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
2022-03-06 11:34:33 +00:00
import { map, Observable } from 'rxjs';
2022-02-24 20:32:31 +00:00
2022-03-28 10:50:39 +00:00
// This interceptor will neatly wrap any json response made within nest
2022-04-04 08:36:59 +00:00
export interface ZodValidationInterceptorOptions {
strict?: boolean;
}
2022-02-24 20:32:31 +00:00
@Injectable()
export class SuccessInterceptor implements NestInterceptor {
2022-04-04 08:36:59 +00:00
private readonly logger = new Logger();
// TODO: make work
private strict: boolean;
constructor(
2022-06-27 15:37:37 +00:00
private readonly reflector: Reflector,
2022-04-04 08:36:59 +00:00
@Optional() options?: ZodValidationInterceptorOptions,
) {
this.strict = options?.strict ?? true;
}
2022-04-05 18:37:25 +00:00
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
2022-02-24 20:32:31 +00:00
return next.handle().pipe(
map((data) => {
if (data instanceof Buffer) {
return data;
} else if (typeof data === 'object') {
2022-04-04 08:36:59 +00:00
const validated = this.validate(context, data);
2022-02-25 11:22:00 +00:00
2022-04-04 08:36:59 +00:00
return this.createResponse(context, validated);
2022-02-24 20:32:31 +00:00
} else {
return data;
}
}),
2022-12-26 14:56:17 +00:00
map((data) => {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse<FastifyReply>();
const traceString = `(${request.ip} -> ${request.method} ${request.url})`;
this.logger.verbose(
`Handled ${traceString} with ${response.statusCode} in ${Math.ceil(
response.getResponseTime(),
)}ms`,
SuccessInterceptor.name,
);
return data;
}),
2022-02-24 20:32:31 +00:00
);
}
2022-04-04 08:36:59 +00:00
2022-04-05 18:37:25 +00:00
private validate(context: ExecutionContext, data: unknown): unknown {
2022-12-25 21:47:43 +00:00
const canReturnAnything =
(this.reflector.get('noreturns', context.getHandler()) ?? false) === true;
if (canReturnAnything) return data;
2022-04-04 08:36:59 +00:00
const schemaStatic = this.reflector.get<ZodDtoStatic>(
'returns',
context.getHandler(),
);
if (!schemaStatic) {
2022-07-19 12:54:02 +00:00
throw Fail(
FT.Internal,
"Couldn't find schema",
2022-04-04 08:36:59 +00:00
`No zodSchema found on handler ${context.getHandler().name}`,
);
}
const schema = schemaStatic.zodSchema;
2022-04-04 08:36:59 +00:00
const parseResult = schema.safeParse(data);
if (!parseResult.success) {
2022-07-19 12:54:02 +00:00
throw Fail(
FT.Internal,
'Server produced invalid response',
2022-06-05 10:20:16 +00:00
`Function ${context.getHandler().name} failed validation: ${
parseResult.error
}`,
2022-04-04 08:36:59 +00:00
);
}
return parseResult.data;
}
2022-06-05 10:20:16 +00:00
private createResponse(
context: ExecutionContext,
data: unknown,
): ApiAnySuccessResponse {
2022-08-26 18:40:16 +00:00
const response = context.switchToHttp().getResponse<FastifyReply>();
const newResponse: ApiAnySuccessResponse = {
success: true as const, // really typescript
2022-08-26 18:40:16 +00:00
statusCode: response.statusCode,
2022-04-04 08:36:59 +00:00
timestamp: new Date().toISOString(),
2022-08-26 18:40:16 +00:00
timeMs: Math.round(response.getResponseTime()),
2022-04-04 08:36:59 +00:00
data,
};
2022-08-26 18:40:16 +00:00
return newResponse;
2022-04-04 08:36:59 +00:00
}
2022-02-24 20:32:31 +00:00
}