feat(v2.3): 完成关系表写入闭环

This commit is contained in:
Script Generator
2026-07-03 13:28:59 +08:00
parent e9ff986bac
commit bba02775dc
17 changed files with 732 additions and 18 deletions

View File

@@ -0,0 +1,69 @@
import { lastValueFrom, of, throwError } from 'rxjs';
import { ApiTimingInterceptor, resolveApiSlowRequestThreshold } from './api-timing.interceptor';
describe('ApiTimingInterceptor', () => {
const originalEnv = process.env.API_SLOW_REQUEST_MS;
afterEach(() => {
process.env.API_SLOW_REQUEST_MS = originalEnv;
jest.restoreAllMocks();
});
it('uses a safe default threshold when env is missing or invalid', () => {
delete process.env.API_SLOW_REQUEST_MS;
expect(resolveApiSlowRequestThreshold()).toBe(1000);
process.env.API_SLOW_REQUEST_MS = 'not-a-number';
expect(resolveApiSlowRequestThreshold()).toBe(1000);
});
it('uses a positive configured threshold', () => {
process.env.API_SLOW_REQUEST_MS = '250';
expect(resolveApiSlowRequestThreshold()).toBe(250);
});
it('logs slow HTTP requests with method and url', async () => {
process.env.API_SLOW_REQUEST_MS = '10';
const interceptor = new ApiTimingInterceptor();
const warn = jest.fn();
(interceptor as any).logger.warn = warn;
jest.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(1025);
const context = {
switchToHttp: () => ({
getRequest: () => ({
method: 'PUT',
originalUrl: '/api/v1/data/dev-tasks',
}),
}),
};
const next = { handle: () => of({ ok: true }) };
await lastValueFrom(interceptor.intercept(context as any, next as any));
expect(warn).toHaveBeenCalledWith(expect.stringContaining('PUT /api/v1/data/dev-tasks'));
expect(warn).toHaveBeenCalledWith(expect.stringContaining('25ms'));
});
it('logs slow HTTP requests even when the handler fails', async () => {
process.env.API_SLOW_REQUEST_MS = '10';
const interceptor = new ApiTimingInterceptor();
const warn = jest.fn();
(interceptor as any).logger.warn = warn;
jest.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(1030);
const context = {
switchToHttp: () => ({
getRequest: () => ({
method: 'PATCH',
url: '/api/v1/data/bugs',
}),
}),
};
const next = { handle: () => throwError(() => new Error('boom')) };
await expect(lastValueFrom(interceptor.intercept(context as any, next as any))).rejects.toThrow('boom');
expect(warn).toHaveBeenCalledWith(expect.stringContaining('PATCH /api/v1/data/bugs'));
expect(warn).toHaveBeenCalledWith(expect.stringContaining('30ms'));
});
});

View File

@@ -0,0 +1,31 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { finalize, Observable } from 'rxjs';
const DEFAULT_API_SLOW_REQUEST_MS = 1000;
export function resolveApiSlowRequestThreshold(): number {
const parsed = Number(process.env.API_SLOW_REQUEST_MS);
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_SLOW_REQUEST_MS;
return Math.floor(parsed);
}
@Injectable()
export class ApiTimingInterceptor implements NestInterceptor {
private readonly logger = new Logger(ApiTimingInterceptor.name);
private readonly thresholdMs = resolveApiSlowRequestThreshold();
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const startedAt = Date.now();
return next.handle().pipe(
finalize(() => {
const durationMs = Date.now() - startedAt;
if (durationMs < this.thresholdMs) return;
const request = context.switchToHttp().getRequest<{ method?: string; originalUrl?: string; url?: string }>();
const method = request.method ?? 'UNKNOWN';
const url = request.originalUrl ?? request.url ?? 'unknown-url';
this.logger.warn(`Slow API request: ${method} ${url} ${durationMs}ms`);
}),
);
}
}