nestjs-prisma-querybuilder
v1.6.5
Published
Prisma QueryBuilder with Nestjs, removing the unused data request and allowing the frontend to choose which data to get, without leaving the REST standard.
Maintainers
Readme
Nestjs/prisma-querybuilder
Documentação / Documentation
English
How to install it?
npm i nestjs-prisma-querybuilder
If there is any CORS configuration in your project, add the properties count and page to your exposedHeaders;
In your app.module include
Querybuilderin providersPrismaServiceis your service. To learn how to create it, read the documentation @nestjs/prisma;// app.module import { Querybuilder } from 'nestjs-prisma-querybuilder'; providers: [PrismaService, QuerybuilderService, Querybuilder],
QuerybuilderServiceis your service and you will use it in your methods;import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Prisma } from '@prisma/client'; import { Querybuilder, QueryResponse } from 'nestjs-prisma-querybuilder'; import { Request } from 'express'; import { PrismaService } from 'src/prisma.service'; @Injectable() export class QuerybuilderService { constructor(@Inject(REQUEST) private readonly request: Request, private readonly querybuilder: Querybuilder, private readonly prisma: PrismaService) {} /** * * @param model model name in schema.prisma; * @param primaryKey primaryKey name for this model in prisma.schema; * @param where object for 'where' using prisma rules; * @param mergeWhere define if the previous where will be merged with the query where or replace it; * @param justPaginate remove any 'select' and 'include' * @param setHeaders define if will set response headers 'count' and 'page' * @param depth limit the depth for filter/populate. default is '_5_' * @param forbiddenFields fields that will be removed from any select/filter/populate/sort * */ async query({ model, depth, where, mergeWhere, justPaginate, forbiddenFields, primaryKey = 'id', setHeaders = true }: { model: Prisma.ModelName; where?: any; depth?: number; primaryKey?: string; mergeWhere?: boolean; setHeaders?: boolean; justPaginate?: boolean; forbiddenFields?: string[]; }): Promise<Partial<QueryResponse>> { return this.querybuilder .query(primaryKey, depth, setHeaders, forbiddenFields) .then(async (query) => { if (where) query.where = mergeWhere ? { ...query.where, ...where } : where; if (setHeaders) { const count = await this.prisma[model].count({ where: query.where }); this.request.res.setHeader('count', count); } if (justPaginate) { delete query.include; delete query.select; } return { ...query }; }) .catch((err) => { if (err.response?.message) throw new BadRequestException(err.response?.message); throw new BadRequestException('Internal error processing your query string, check your parameters'); }); } }
How to use it?
You can use this frontend interface to make your queries easier -- Nestjs prisma querybuilder interface
Add your QuerybuilderService in any service:
// service constructor(private readonly prisma: PrismaService, private readonly qb: QuerybuilderService) {}Configure your method:
- The
querymethod will build the query with your @Query() fromREQUEST, but you don't need to send it as a parameter; - The
querywill append to theResponse.headersacountproperty with the total number of objects found (including pagination) - The
querywill receive one string with your model name, this will be used to make the count;
async UserExample() { const query = await this.qb.query('User'); return this.prisma.user.findMany(query); }- The
Available parameters:
Example models:
model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String published Boolean? @default(false) author User? @relation(fields: [authorId], references: [id]) authorId Int? content Content[] @@map("posts") } model Content { id Int @id @default(autoincrement()) text String post Post @relation(fields: [postId], references: [id]) postId Int @@map("contents") }Page and Limit
- By default pagination is always enabled and if consumer doesn't send
pageandlimitin query, it will return page 1 with 10 items; - The
Response.headerswill have the propertiescountandpagewith total items and page number; http://localhost:3000/posts?page=2&limit=10
- By default pagination is always enabled and if consumer doesn't send
Sort
- To use
sortyou need two properties:criteriaandfield; criteriais an enum withascanddesc;fieldis the field that sort will be applied to;http://localhost:3000/posts?sort[criteria]=asc&sort[field]=title
- To use
Distinct
- All properties should be separated by blank space, comma or semicolon;
- To use
distinctyou only need a string; http://localhost:3000/posts?distinct=title published
Select
All properties should be separated by blank space, comma or semicolon;
By default if you don't send any
selectthe query will only return theidproperty;If you need to get the whole object you can use
select=all;Exception: If you select a relationship field it will return the entire object. To select fields in a relation you can use
populate, and to get just itsidyou can use theauthorIdfield;http://localhost:3000/posts?select=id title,published;authorIdTo exclude fields from the return, you can use a DTO on the prisma response before returning to the user OR use the 'forbiddenFields' parameter in the query method;
- Example: a user password or token information;
- When using forbiddenFields, select 'all' will be ignored;
Populate
- Populate is an array that allows you to select fields from relationships. It needs two parameters:
pathandselect; pathis the relationship reference (ex: author);selectcontains the fields that will be returned;select=allis not supported by populate
primaryKeyis the reference to the primary key of the relationship (optional) (default: 'id');- The populate index is needed to link the
pathandselectproperties; http://localhost:3000/posts?populate[0][path]=author&populate[0][select]=name email
- Populate is an array that allows you to select fields from relationships. It needs two parameters:
Filter
- Can be used to filter the query with your requirements
pathis a reference to the property that will have the filter applied;valueis the value that will be filtered;filterGroupcan be used to make where clauses with operatorsand,orandnotor no operator (optional);- accepted types:
['and', 'or', 'not']
- accepted types:
operatorcan be used to customize your filter (optional);- accepted types:
['contains', 'endsWith', 'startsWith', 'equals', 'gt', 'gte', 'in', 'lt', 'lte', 'not', 'notIn', 'hasEvery', 'hasSome', 'has', 'isEmpty'] hasEvery, hasSome and notIntake a single string with values separated by blank space?filter[0][path]=name&filter[0][operator]=hasSome&filter[0][value]=foo bar ula
- accepted types:
insensitivecan be used for case-insensitive filtering (optional);- accepted types:
['true', 'false'] - default: 'false' - (check prisma rules for more details - Prisma: Database collation and case sensitivity)
- accepted types:
typeneeds to be used if value is not a string;- accepted types:
['string', 'boolean', 'number', 'date', 'object'] - default: 'string'- 'object' accepted values: ['null', 'undefined']
- accepted types:
- filter is an array that allows you to append multiple filters to the same query;
http://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[1][path]=published&filter[1][value]=falsehttp://localhost:3000/posts?filter[1][path]=published&filter[1][value]=false&filter[1][type]=booleanhttp://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[0][filterGroup]=and&filter[1][path]=published&filter[1][value]=false&filter[1][filterGroup]=and
Português
Como instalar?
npm i nestjs-prisma-querybuilder
No seu app.module inclua o
Querybuilderaos providers:PrismaServiceé o seu service, para ver como criar ele leia a documentação @nestjs/prisma;// app.module import { Querybuilder } from 'nestjs-prisma-querybuilder'; providers: [PrismaService, QuerybuilderService, Querybuilder],QuerybuilderServicevai ser o service que será usado nos seus métodos;import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Prisma } from '@prisma/client'; import { Querybuilder, QueryResponse } from 'nestjs-prisma-querybuilder'; import { Request } from 'express'; import { PrismaService } from 'src/prisma.service'; @Injectable() export class QuerybuilderService { constructor(@Inject(REQUEST) private readonly request: Request, private readonly querybuilder: Querybuilder, private readonly prisma: PrismaService) {} /** * * @param model nome do model no schema.prisma; * @param primaryKey nome da chave primaria deste model no prisma.schema; * @param where objeto para where de acordo com as regras do prisma; * @param mergeWhere define se o where informado no parâmetro anterior será unido ou substituirá um possivel where vindo da query; * @param justPaginate remove qualquer 'select' e 'populate' da query; * @param setHeaders define se será adicionado os headers 'count' e 'page' na resposta; * @param depth limita o numero de 'niveis' que a query vai lhe permitir fazer (filter/populate). default is '_5_' * @param forbiddenFields campos que serão removidos de qualquer select/filter/populate/sort */ async query({ model, depth, where, mergeWhere, justPaginate, forbiddenFields, primaryKey = 'id', setHeaders = true }: { model: Prisma.ModelName; where?: any; depth?: number; primaryKey?: string; mergeWhere?: boolean; setHeaders?: boolean; justPaginate?: boolean; forbiddenFields?: string[]; }): Promise<Partial<QueryResponse>> { return this.querybuilder .query(primaryKey, depth, setHeaders, forbiddenFields) .then(async (query) => { if (where) query.where = mergeWhere ? { ...query.where, ...where } : where; if (setHeaders) { const count = await this.prisma[model].count({ where: query.where }); this.request.res.setHeader('count', count); } if (onlyPaginate) { delete query.include; delete query.select; } return { ...query }; }) .catch((err) => { if (err.response?.message) throw new BadRequestException(err.response?.message); throw new BadRequestException('Internal error processing your query string, check your parameters'); }); } }
Optional: Você pode adicionar uma validação adicional para o parametro
model, mas essa validação vai variar de acordo com o seu database;Exemplo com
SQLiteif (!this.tables?.length) this.tables = await this.prisma.$queryRaw`SELECT name FROM sqlite_schema WHERE type ='table' AND name NOT LIKE 'sqlite_%';`; if (!this.tables.find((v) => v.name === model)) throw new BadRequestException('Invalid model');
Como usar?
Você pode usar essa interface para tornar suas queries mais fácies no frontend -- Nestjs prisma querybuilder interface
Adicione o Querybuilder no seu service:
// service constructor(private readonly prisma: PrismaService, private readonly qb: QuerybuilderService) {}Configurando seu método:
- o método
queryvai montar a query baseada no @Query(), mas o mesmo é pego direto doREQUEST, não sendo necessário passar como parâmetro; - o método
queryjá vai adicionar noResponse.headersa propriedadecountque vai ter o total de objetos encontrados (usado para paginação); - o método
queryrecebe uma string com o nome referente ao model, isso vai ser usado para fazer o count;
async UserExemple() { const query = await this.qb.query('User'); return this.prisma.user.findMany(query); }- o método
Parametros disponiveis:
models de exemplo:
model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String published Boolean? @default(false) author User? @relation(fields: [authorId], references: [id]) authorId Int? content Content[] @@map("posts") } model Content { id Int @id @default(autoincrement()) text String post Post @relation(fields: [postId], references: [id]) postId Int @@map("contents") }Page e Limit
- Por padrão a páginação está sempre habilitada e se não enviado
pageelimitna query, vai ser retornado página 1 com 10 itens; - Nos headers da response haverá a propriedade
countcom o total de itens a serem paginados; http://localhost:3000/posts?page=2&limit=10
- Por padrão a páginação está sempre habilitada e se não enviado
Sort
- Para montar o sort são necessário enviar duas propriedades
fieldecriteria; - criteria é um enum com ['asc', 'desc'];
- field é o campo pelo qual a ordenação vai ser aplicada;
http://localhost:3000/posts?sort[criteria]=asc&sort[field]=title
- Para montar o sort são necessário enviar duas propriedades
Distinct
- Todas as propriedades devem ser separadas por espaço em branco, virgula ou ponto e virgula;
- Para montar o distinct é necessário enviar apenas os valores;
http://localhost:3000/posts?distinct=title published
Select
Todas as propriedades devem ser separadas por espaço em branco, virgula ou ponto e virgula;
Por padrão se não for enviado nenhum select qualquer busca só irá retornar a propriedade
idSe for necessário pegar todo o objeto é possível usar
select=all,Exceção: ao dar select em um relacionamento será retornado todo o objeto do relacionamento, para usar o select em um relacionamento use o
populate, para buscar somente oidde um relacionamento é possível usar a colunaauthorIdhttp://localhost:3000/posts?select=id title,published;authorIdPara excluir campos no retorno, você pode utilizar um DTO na resposta do prisma antes de devolve-lá ao usuário OU usar o parametro 'forbiddenFields' no método query ;
- Exemplo uma senha de usuário ou informações de tokens;
- Ao usar forbiddenFields select 'all' será ignorado;
Populate
- Populate é um array que permite dar select nos campos dos relacionamentos, é composto por 2 parametros, path e select;
pathé a referencia para qual relacionamento será populado;selectsão os campos que irão serem retornados;select=allnão é suportado no populate
primaryKeynome da chave primaria do relacionamento (opcional) (default: 'id');- Podem ser feitos todos os populates necessários usando o índice do array para ligar o path ao select;
http://localhost:3000/posts?populate[0][path]=author&populate[0][select]=name email
Filter
- Pode ser usado para filtrar a consulta com os parâmetros desejados;
pathé a referencia para qual propriedade irá aplicar o filtro;valueé o valor pelo qual vai ser filtrado;filterGroupPode ser usado para montar o where usando os operadores ['AND', 'OR', 'NOT'] ou nenhum operador (opcional);- opções:
['and', 'or, 'not']
- opções:
operatorpode ser usado para personalizar a consulta (opcional);- recebe os tipos
['contains', 'endsWith', 'startsWith', 'equals', 'gt', 'gte', 'in', 'lt', 'lte', 'not', 'notIn', 'hasEvery', 'hasSome', 'has', 'isEmpty'] hasEvery, hasSome e notInrecebe uma unica string separando os valores por um espaço em branco?filter[0][path]=name&filter[0][operator]=hasSome&filter[0][value]=foo bar ula
- recebe os tipos
insensitivepode ser usado para personalizar a consulta (opcional);- recebe os tipos:
['true', 'false'] - default: 'false' - (confira as regras do prisma para mais informações - Prisma: Database collation and case sensitivity)
- recebe os tipos:
typeé usado caso o valor do filter NÃO seja do tipo 'string'- recebe os tipos:
['string', 'boolean', 'number', 'date' , 'object'] - default: 'string'- 'object' recebe os valores: ['null', 'undefined']
- recebe os tipos:
- filter é um array, podendo ser adicionados vários filtros de acordo com a necessidade da consulta;
- consulta simples
http://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[1][path]=published&filter[1][value]=falsehttp://localhost:3000/posts?filter[1][path]=published&filter[1][value]=false&filter[1][type]=booleanhttp://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[0][filterGroup]=and&filter[1][path]=published&filter[1][value]=falsefilter[1][filterGroup]=and
- Nestjs/Prisma Querybuilder is MIT licensed.
