Prisma QueryBuilder with Nestjs, removing the unused data request and allowing the frontend to choose which data to get, without leaving the REST standard.
npm install nestjs-prisma-querybuilder!https://nestjs.com/img/logo_text.svg
- 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 Querybuilder in providers
- PrismaService is your service. To learn how to create it, read the documentation @nestjs/prisma;
``tsx
// app.module
import { Querybuilder } from 'nestjs-prisma-querybuilder';
providers: [PrismaService, QuerybuilderService, Querybuilder],
`
- QuerybuilderService is your service and you will use it in your methods;
`tsx
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
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:
`tsx`
// service
constructor(private readonly prisma: PrismaService, private readonly qb: QuerybuilderService) {}
- Configure your method:
- The query method will build the query with your @Query() from REQUEST, but you don't need to send it as a parameter;query
- The will append to the Response.headers a count property with the total number of objects found (including pagination)query
- The will receive one string with your model name, this will be used to make the count;
`jsx
async UserExample() {
const query = await this.qb.query('User');
return this.prisma.user.findMany(query);
}
`
- Available parameters:
- Example models:
`jsx
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 page and limit in query, it will return page 1 with 10 items;Response.headers
- The will have the properties count and page with total items and page number;http://localhost:3000/posts?page=2&limit=10
- sort
- Sort
- To use you need two properties: criteria and field;criteria
- is an enum with asc and desc;field
- is the field that sort will be applied to;http://localhost:3000/posts?sort[criteria]=asc&sort[field]=title
- distinct
- Distinct
- All properties should be separated by blank space, comma or semicolon;
- To use you 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 select the query will only return the id property;select=all
- If you need to get the whole object you can use ;populate
- Exception: If you select a relationship field it will return the entire object. To select fields in a relation you can use , and to get just its id you can use the authorId field;http://localhost:3000/posts?select=id title,published;authorId
-
- To 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: path and select;
- path is the relationship reference (ex: author);select
- contains the fields that will be returned;select=all
- is not supported by populateprimaryKey
- is the reference to the primary key of the relationship (optional) (default: 'id');path
- The populate index is needed to link the and select properties;http://localhost:3000/posts?populate[0][path]=author&populate[0][select]=name email
- path
- Filter
- Can be used to filter the query with your requirements
- is a reference to the property that will have the filter applied;value
- is the value that will be filtered;filterGroup
- can be used to make where clauses with operators and, or and not or no operator (optional);['and', 'or', 'not']
- accepted types: operator
- can be used to customize your filter (optional);['contains', 'endsWith', 'startsWith', 'equals', 'gt', 'gte', 'in', 'lt', 'lte', 'not', 'notIn', 'hasEvery', 'hasSome', 'has', 'isEmpty']
- accepted types: hasEvery, hasSome and notIn
- take a single string with values separated by blank space?filter[0][path]=name&filter[0][operator]=hasSome&filter[0][value]=foo bar ula
- insensitive
- can be used for case-insensitive filtering (optional);['true', 'false'] - default: 'false'
- accepted types: type
- (check prisma rules for more details - Prisma: Database collation and case sensitivity)
- needs to be used if value is not a string;['string', 'boolean', 'number', 'date', 'object'] - default: 'string'
- accepted types: http://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[1][path]=published&filter[1][value]=false
- 'object' accepted values: ['null', 'undefined']
- filter is an array that allows you to append multiple filters to the same query;
- http://localhost:3000/posts?filter[1][path]=published&filter[1][value]=false&filter[1][type]=boolean
- http://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
-
- Como instalar?
- npm i nestjs-prisma-querybuilderQuerybuilder
- No seu app.module inclua o aos providers:
- PrismaService é o seu service, para ver como criar ele leia a documentação @nestjs/prisma;
`tsx
// app.module
import { Querybuilder } from 'nestjs-prisma-querybuilder';
providers: [PrismaService, QuerybuilderService, Querybuilder],
`
- QuerybuilderService vai ser o service que será usado nos seus métodos;
`tsx
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
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 SQLite
`tsxSELECT name FROM sqlite_schema WHERE type ='table' AND name NOT LIKE 'sqlite_%';
if (!this.tables?.length) this.tables = await this.prisma.$queryRaw;
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:
`tsx`
// service
constructor(private readonly prisma: PrismaService, private readonly qb: QuerybuilderService) {}
- Configurando seu método:
- o método query vai montar a query baseada no @Query(), mas o mesmo é pego direto do REQUEST, não sendo necessário passar como parâmetro;query
- o método já vai adicionar no Response.headers a propriedade count que vai ter o total de objetos encontrados (usado para paginação);query
- o método recebe uma string com o nome referente ao model, isso vai ser usado para fazer o count;
`jsx
async UserExemple() {
const query = await this.qb.query('User');
return this.prisma.user.findMany(query);
}
`
- Parametros disponiveis:
- models de exemplo:
`jsx
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 page e limit na query, vai ser retornado página 1 com 10 itens;count
- Nos headers da response haverá a propriedade com o total de itens a serem paginados;http://localhost:3000/posts?page=2&limit=10
- field
- Sort
- Para montar o sort são necessário enviar duas propriedades e criteria;http://localhost:3000/posts?sort[criteria]=asc&sort[field]=title
- criteria é um enum com ['asc', 'desc'];
- field é o campo pelo qual a ordenação vai ser aplicada;
- http://localhost:3000/posts?distinct=title published
- 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;
-
- 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 idselect=all
- Se for necessário pegar todo o objeto é possível usar ,populate
- Exceção: ao dar select em um relacionamento será retornado todo o objeto do relacionamento, para usar o select em um relacionamento use o , para buscar somente o id de um relacionamento é possível usar a coluna authorIdhttp://localhost:3000/posts?select=id title,published;authorId
-
- Para 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;select
- são os campos que irão serem retornados;select=all
- não é suportado no populateprimaryKey
- nome da chave primaria do relacionamento (opcional) (default: 'id');http://localhost:3000/posts?populate[0][path]=author&populate[0][select]=name email
- Podem ser feitos todos os populates necessários usando o índice do array para ligar o path ao select;
- path
- Filter
- Pode ser usado para filtrar a consulta com os parâmetros desejados;
- é a referencia para qual propriedade irá aplicar o filtro;value
- é o valor pelo qual vai ser filtrado;filterGroup
- Pode ser usado para montar o where usando os operadores ['AND', 'OR', 'NOT'] ou nenhum operador (opcional);['and', 'or, 'not']
- opções: operator
- pode ser usado para personalizar a consulta (opcional);['contains', 'endsWith', 'startsWith', 'equals', 'gt', 'gte', 'in', 'lt', 'lte', 'not', 'notIn', 'hasEvery', 'hasSome', 'has', 'isEmpty']
- recebe os tipos hasEvery, hasSome e notIn
- recebe 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
- insensitive
- pode ser usado para personalizar a consulta (opcional);['true', 'false'] - default: 'false'
- recebe os tipos: type
- (confira as regras do prisma para mais informações - Prisma: Database collation and case sensitivity)
- é usado caso o valor do filter NÃO seja do tipo 'string'['string', 'boolean', 'number', 'date' , 'object'] - default: 'string'
- recebe os tipos: http://localhost:3000/posts?filter[0][path]=title&filter[0][value]=querybuilder&filter[1][path]=published&filter[1][value]=false
- 'object' recebe os valores: ['null', 'undefined']
- filter é um array, podendo ser adicionados vários filtros de acordo com a necessidade da consulta;
- consulta simples
- http://localhost:3000/posts?filter[1][path]=published&filter[1][value]=false&filter[1][type]=boolean
- http://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.