如何使用Nestjs创建分页,可分类和可过滤的端点
#javascript #node #api #nest

Photo by Sophie Elvis on Unsplash

分页,排序和过滤是常见的技术,用于处理列表端点时,用于处理由API 返回的大量数据。它们对于优化性能,改善用户体验和减少服务器(以及客户端)负载至关重要。让我们在深入一些Nestjs代码之前简要讨论这个概念。

分页

包括将大数据集分为较小的页面并以增量,有条理的方式将这些数据提供给客户的过程。这提高了客户端的渲染速度,并减少了通过网络发送的数据量。

假设您的数据库上有一个城市桌。例如,巴西拥有超过5.500个城市。每次用户要求它时,所有这些都不合理。相反,我们可以一次发送20个,并在前端上相应地渲染它们(例如表或下拉列表),这将负责请求正确的数据。

以及前端如何索取正确的数据?很简单,我们通常使用查询参数,例如 page size 来指定哪个页面以及有多少结果来获取

在分页的资源响应中,通常包括附加元数据以及获取的数据以提供有关分页的相关信息。这样做可以使客户更好地协调请求。例如,我们可以返回总项目计数和总页数计数,然后客户知道仍有多少页可以获取。

一个简单的例子

让我们了解这将如何与我们的城市表一起使用。假设我们的数据库看起来像以下内容。

âo paul
id 名称 state_id
1 1
2 santos 1
3 campinas 1
4 里约热内卢 2
5 niterã³i 2
6 Belo Horizo​​nte 3
7 brasãlia 4
8 curitiba 5
9 Porto Alegre 6
10 florianã³polis 7

如果 getCities 端点正确分页,我们可以提出以下请求:

  • https://our-api/v1/cities?page=0&size=2:wold返回£o保罗和桑托斯
  • santos 。
  • https://our-api/v1/cities?page=1&size=2:将返回Campinas和Rio de Janeiro
  • https://our-api/v1/cities?page=0&size=4:将退还他们四个。

等等。

分类和过滤

排序是订购根据特定字段的特定标准数据。过滤是关于选择特定数据条目,具体取决于某些其他标准,也基于特定字段。

您可能已经猜到了,因为我们正在将我们的资源在后端上铺上,所以这是我们还需要进行分类和过滤的地方。由于所有数据都不存在。

为了插图,假设我们有一个桌子(前端),可以使我们的分页城市。如果我们获取页面= 0且大小= 2的数据,我们将接收圣£o Paulo和Santos。如果我们尝试按字母顺序排序,在客户端上,我们将获得相同的结果(因为客户只有此片段的数据集),这是不正确的,结果应该是Belo Horizo​​nte和Campinas。

在这种情况下,当我们尝试对客户端上的表进行分类时,我们应该提出一个新请求,指定我们仍然需要两个条目,但是我们需要先按字母顺序排序。

奠定基础

让我们开始弄脏我们的手。首先,我们将创建一些功能和自定义装饰器将帮助我们完成任务。

@PaginationParams()

这个第一个装饰器(信用我的好朋友Caio Argentino )负责从HTTP请求查询字符串中提取和验证分页参数。

import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

export interface Pagination {
    page: number;
    limit: number;
    size: number;
    offset: number;
}

export const PaginationParams = createParamDecorator((data, ctx: ExecutionContext): Pagination => {
    const req: Request = ctx.switchToHttp().getRequest();
    const page = parseInt(req.query.page as string);
    const size = parseInt(req.query.size as string);

    // check if page and size are valid
    if (isNaN(page) || page < 0 || isNaN(size) || size < 0) {
        throw new BadRequestException('Invalid pagination params');
    }
    // do not allow to fetch large slices of the dataset
    if (size > 100) {
        throw new BadRequestException('Invalid pagination params: Max size is 100');
    }

    // calculate pagination parameters
    const limit = size;
    const offset = page * limit;
    return { page, limit, size, offset };
});

从查询中提取页面和大小并验证它后,装饰器返回一个对象,其中包含要处理多少项的信息(限制)以及将跳过多少项(偏移)。

@SortingParams(录音params)

此装饰器负责将以paramName:direction形式的查询参数解析为对象,从而验证方向和参数是否有效。 direction的有效值是ascdesc,有效参数是控制器发送的一系列字符串。

import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

export interface Sorting {
    property: string;
    direction: string;
}

export const SortingParams = createParamDecorator((validParams, ctx: ExecutionContext): Sorting => {
    const req: Request = ctx.switchToHttp().getRequest();
    const sort = req.query.sort as string;
    if (!sort) return null;

    // check if the valid params sent is an array
    if (typeof validParams != 'object') throw new BadRequestException('Invalid sort parameter');

    // check the format of the sort query param
    const sortPattern = /^([a-zA-Z0-9]+):(asc|desc)$/;
    if (!sort.match(sortPattern)) throw new BadRequestException('Invalid sort parameter');

    // extract the property name and direction and check if they are valid
    const [property, direction] = sort.split(':');
    if (!validParams.includes(property)) throw new BadRequestException(`Invalid sort property: ${property}`);

    return { property, direction };
});

@FilteringParms(vallParams)

此装饰器负责解析过滤参数(在此示例中,我们只能一次过滤一列),以paramName:rule:value的格式出现,并类似于最后一个。

import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

export interface Filtering {
    property: string;
    rule: string;
    value: string;
}

// valid filter rules
export enum FilterRule {
    EQUALS = 'eq',
    NOT_EQUALS = 'neq',
    GREATER_THAN = 'gt',
    GREATER_THAN_OR_EQUALS = 'gte',
    LESS_THAN = 'lt',
    LESS_THAN_OR_EQUALS = 'lte',
    LIKE = 'like',
    NOT_LIKE = 'nlike',
    IN = 'in',
    NOT_IN = 'nin',
    IS_NULL = 'isnull',
    IS_NOT_NULL = 'isnotnull',
}

export const FilteringParams = createParamDecorator((data, ctx: ExecutionContext): Filtering => {
    const req: Request = ctx.switchToHttp().getRequest();
    const filter = req.query.filter as string;
    if (!filter) return null;

    // check if the valid params sent is an array
    if (typeof data != 'object') throw new BadRequestException('Invalid filter parameter');

    // validate the format of the filter, if the rule is 'isnull' or 'isnotnull' it don't need to have a value
    if (!filter.match(/^[a-zA-Z0-9_]+:(eq|neq|gt|gte|lt|lte|like|nlike|in|nin):[a-zA-Z0-9_,]+$/) && !filter.match(/^[a-zA-Z0-9_]+:(isnull|isnotnull)$/)) {
        throw new BadRequestException('Invalid filter parameter');
    }

    // extract the parameters and validate if the rule and the property are valid
    const [property, rule, value] = filter.split(':');
    if (!data.includes(property)) throw new BadRequestException(`Invalid filter property: ${property}`);
    if (!Object.values(FilterRule).includes(rule as FilterRule)) throw new BadRequestException(`Invalid filter rule: ${rule}`);

    return { property, rule, value };
});

Typeorm助手

最后但并非最不重要的一点是,我们将编写一些辅助功能来生成我们的,其中对象和我们的 order> order> 对typeorm repository方法使用的对象。

import { IsNull, Not, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, ILike, In } from "typeorm";

import { Filtering } from "src/helpers/decorators/filtering-params.decorator"
import { Sorting } from "src/helpers/decorators/sorting-params.decorator";
import { FilterRule } from "src/helpers/decorators/filtering-params.decorator";

export const getOrder = (sort: Sorting) => sort ? { [sort.property]: sort.direction } : {};

export const getWhere = (filter: Filtering) => {
    if (!filter) return {};

    if (filter.rule == FilterRule.IS_NULL) return { [filter.property]: IsNull() };
    if (filter.rule == FilterRule.IS_NOT_NULL) return { [filter.property]: Not(IsNull()) };
    if (filter.rule == FilterRule.EQUALS) return { [filter.property]: filter.value };
    if (filter.rule == FilterRule.NOT_EQUALS) return { [filter.property]: Not(filter.value) };
    if (filter.rule == FilterRule.GREATER_THAN) return { [filter.property]: MoreThan(filter.value) };
    if (filter.rule == FilterRule.GREATER_THAN_OR_EQUALS) return { [filter.property]: MoreThanOrEqual(filter.value) };
    if (filter.rule == FilterRule.LESS_THAN) return { [filter.property]: LessThan(filter.value) };
    if (filter.rule == FilterRule.LESS_THAN_OR_EQUALS) return { [filter.property]: LessThanOrEqual(filter.value) };
    if (filter.rule == FilterRule.LIKE) return { [filter.property]: ILike(`%${filter.value}%`) };
    if (filter.rule == FilterRule.NOT_LIKE) return { [filter.property]: Not(ILike(`%${filter.value}%`)) };
    if (filter.rule == FilterRule.IN) return { [filter.property]: In(filter.value.split(',')) };
    if (filter.rule == FilterRule.NOT_IN) return { [filter.property]: Not(In(filter.value.split(','))) };
}

此功能基本上是根据装饰器返回的属性创建对象的。例如,如果过滤器是city:like:Campinas,我们会得到:

{
    city: ILike(`%Campinas%`)
}

我们还需要一个特定的DTO来使用分页资源返回数据

export type PaginatedResource<T> = {
    totalItems: number;
    items: T[];
    page: number;
    size: number;
};

创建端点

最后,让我们使用我们在控制器和服务中创建的东西来使其一切正常!我们将为我们的城市示例编写代码。控制器应该看起来像这样。

@Controller('cities')
export class CitiesController {
    private readonly logger = new Logger(CitiesController.name);

    constructor(
        private readonly cityService: CityService,
    ) { }

    @Get()
    @HttpCode(HttpStatus.OK)
    public async getCities(
        @PaginationParams() paginationParams: Pagination,
        @SortingParams(['name', 'id', 'stateId']) sort?: Sorting,
        @FilteringParams(['name', 'id', 'stateId']) filter?: Filtering
    ): Promise<PaginatedResource<Partial<City>>> {
        this.logger.log(`REST request to get cities: ${JSON.stringify(paginationParams)}, ${sort}, ${filter}`);
        return await this.cityService.getCities(paginationParams, sort, filter);
    }
}

现在,我们能够使用我们创建的装饰器来提取我们服务将使用的参数来分页,分类和过滤我们的数据。我们的服务看起来像这样。

@Injectable()
export class CityService {
    constructor(
        @InjectRepository(City)
        private readonly cityRepository: Repository<City>,
    ) { }

    public async getCities(
        { page, limit, size, offset }: Pagination,
        sort?: Sorting,
        filter?: Filtering,
    ): Promise<PaginatedResource<Partial<Language>>> {
        const where = getWhere(filter);
        const order = getOrder(sort);

        const [languages, total] = await this.cityRepository.findAndCount({
            where,
            order,
            take: limit,
            skip: offset,
        });

        return {
            totalItems: total,
            items: languages,
            page,
            size
        };
    }
}

,我们有自己的分页,可分类和可过滤的端点!这是一个简单的例子,我在开发一些个人项目时已经锻炼了,我希望它可以帮助某人在此概念 /实现中遇到困难。随时发表评论,并就如何改进提出建议。也可以在Twitter @kogab_:)

上关注我