- Introduced `@Pagination()` decorator to mark list endpoints for pagination.
- Added `TransformInterceptor` to wrap responses in a standardized format `{ data, meta }`.
- Created pagination-related utility functions and constants for managing pagination logic.
- Defined `PaginationQueryDto` for handling pagination query parameters.
- Established `PaginationMetaDto` for OpenAPI documentation of pagination metadata.
- Updated existing controller and service structures to support pagination in responses.
- Added unit tests for pagination utilities and interceptor to ensure correct functionality.
117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
import { CallHandler, ExecutionContext, StreamableFile } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { of, firstValueFrom } from 'rxjs';
|
|
import { PAGINATION_RESPONSE, RAW_RESPONSE } from './constants';
|
|
import { TransformInterceptor } from './transform.interceptor';
|
|
|
|
describe('TransformInterceptor', () => {
|
|
const reflector = {
|
|
getAllAndOverride: jest.fn(),
|
|
};
|
|
const interceptor = new TransformInterceptor(
|
|
reflector as unknown as Reflector,
|
|
);
|
|
|
|
function createContext(
|
|
query: Record<string, unknown> = {},
|
|
): ExecutionContext {
|
|
return {
|
|
getHandler: () => jest.fn(),
|
|
getClass: () => class TestController {},
|
|
switchToHttp: () => ({
|
|
getRequest: () => ({ query }),
|
|
}),
|
|
} as unknown as ExecutionContext;
|
|
}
|
|
|
|
function createHandler(payload: unknown): CallHandler {
|
|
return { handle: () => of(payload) };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
reflector.getAllAndOverride.mockReset();
|
|
});
|
|
|
|
it('passes through when not marked as pagination', async () => {
|
|
reflector.getAllAndOverride.mockReturnValue(false);
|
|
const payload = { id: '1' };
|
|
|
|
const result = await firstValueFrom(
|
|
interceptor.intercept(createContext(), createHandler(payload)),
|
|
);
|
|
|
|
expect(result).toEqual(payload);
|
|
});
|
|
|
|
it('passes through when marked @RawResponse()', async () => {
|
|
reflector.getAllAndOverride.mockImplementation((key: string) => {
|
|
if (key === RAW_RESPONSE) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
const payload = { data: [], total: 0 };
|
|
|
|
const result = await firstValueFrom(
|
|
interceptor.intercept(createContext(), createHandler(payload)),
|
|
);
|
|
|
|
expect(result).toEqual(payload);
|
|
});
|
|
|
|
it('wraps @Pagination() handler return as { data, meta }', async () => {
|
|
reflector.getAllAndOverride.mockImplementation((key: string) => {
|
|
if (key === RAW_RESPONSE) {
|
|
return false;
|
|
}
|
|
if (key === PAGINATION_RESPONSE) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
const result = await firstValueFrom(
|
|
interceptor.intercept(
|
|
createContext({ page: '2', limit: '5' }),
|
|
createHandler({ data: [{ id: 'a' }], total: 11 }),
|
|
),
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
data: [{ id: 'a' }],
|
|
meta: {
|
|
currentPage: 2,
|
|
itemCount: 1,
|
|
itemsPerPage: 5,
|
|
totalItems: 11,
|
|
totalPages: 3,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('does not wrap null payloads', async () => {
|
|
reflector.getAllAndOverride.mockImplementation((key: string) => {
|
|
return key === PAGINATION_RESPONSE;
|
|
});
|
|
|
|
const result = await firstValueFrom(
|
|
interceptor.intercept(createContext(), createHandler(null)),
|
|
);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('does not wrap StreamableFile payloads', async () => {
|
|
reflector.getAllAndOverride.mockImplementation((key: string) => {
|
|
return key === PAGINATION_RESPONSE;
|
|
});
|
|
const file = new StreamableFile(Buffer.from('x'));
|
|
|
|
const result = await firstValueFrom(
|
|
interceptor.intercept(createContext(), createHandler(file)),
|
|
);
|
|
|
|
expect(result).toBe(file);
|
|
});
|
|
});
|