Adapter TypeORM
Use o nestjs-rest-query com TypeORM - o adapter de referência.
O TypeORM é o adapter de referência: é contra ele que os outros dois são comparados. Ele não é um default, porém — não existe adapter default na v3.
Instalação
pnpm add typeorm @nestjs/typeormA faixa suportada é ^0.3.26 || ^1.0.0; as duas majors passam o corpus de
paridade. O typeorm é peer dependency opcional, então ele não é instalado por
você.
Configuração do módulo
import { Module } from '@nestjs/common';
import { DynamicQueryBuilderModule } from 'nestjs-rest-query';
@Module({
imports: [
DynamicQueryBuilderModule.forRoot({
pagination: { defaultPerPage: 10, maxPerPage: 500 },
}),
],
})
export class AppModule {}Nenhum adapter entra aqui. forRoot({ adapter: new TypeOrmAdapter() }) é
recusado na inicialização com SOURCE_CONFIGURATION_INVALID.
Uso
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
DynamicQueryDto,
QueryBuilderService,
type CompiledQueryRules,
type NormalizedQueryResult,
} from 'nestjs-rest-query';
import { typeormSource } from 'nestjs-rest-query/typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersBusiness {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly queryBuilderService: QueryBuilderService
) {}
async findAll(
query: DynamicQueryDto,
rules: CompiledQueryRules
): Promise<NormalizedQueryResult<User>> {
return this.queryBuilderService.execute(
typeormSource(this.userRepository),
query,
rules
);
}
}O typeormSource infere o tipo da linha do repositório que você entrega, então
a anotação Promise<NormalizedQueryResult<User>> não precisa de argumento de
tipo aqui. O prismaSource<TRow> e o drizzleSource<TRow> também são
genéricos no tipo da linha; o que muda é por onde o tipo chega.
Opções da source
typeormSource(repository, {
// Tipos lógicos que o banco não expressa — um char(36) que guarda UUID.
fieldKinds: { user: { id: 'uuid' } },
// Fatos do perfil certificado, coletados uma vez na subida.
portabilityProfile: facts,
});Derivando o schema
Este é o único adapter com um derivador de schema:
import { buildSchemaRegistry } from 'nestjs-rest-query/typeorm';
const registry = buildSchemaRegistry(userRepository, {
fieldKinds: { user: { id: 'uuid' } },
});Ele percorre o grafo de relações transitivamente, então todo model alcançável
cai no registry. A restrição está em onde ele pode rodar: exige um
Repository vivo, e as regras do endpoint são consumidas por
@ApiDynamicQuery(rules) — um decorator de método, avaliado quando a classe do
controller carrega, antes de o Nest montar o container. Dentro de uma factory de
provider ele é utilizável; ao lado de um decorator, não — e é por isso que os
quatro exemplos declaram o schema à mão.
O correspondente do Drizzle é o buildSourceSchema, que deriva do descritor
declarado e não do ORM. O Prisma não tem equivalente nenhum.
Convenções que o resolver cobra
O schema declarado é comparado com o derivado, campo a campo, antes da primeira
query. São comparados: model, primaryKey, e por campo kind, nullable,
primaryKey, foldedField, portableOrderField, internal; por relação
target, cardinality, nullable. Qualquer diferença é
SOURCE_CONFIGURATION_INVALID. Só o schema do root é comparado — os
schemas das relações no registry não são.
Então três detalhes não são escolha de estilo:
Nome das colunas companheiras. O resolver reconhece a companheira dobrada
como <path do campo>_folded e a de ordem portável como <path do campo>_order,
sobre o nome da propriedade da entidade, não sobre a coluna física. Para
firstName mapeado em first_name, a propriedade tem de ser
firstName_folded — a coluna física pode continuar first_name_folded.
Qualquer propriedade cujo nome termine em _folded ou _order é marcada como
interna automaticamente.
@Column({ name: 'first_name' })
firstName: string;
@Column({ name: 'first_name_folded', select: false })
firstName_folded: string;O nome do model é derivado, não escolhido:
metadata.name.replace(/Entity$/, '').toLowerCase(). AccessRequestItem vira
accessrequestitem — não access_request_item, não accessRequestItem. A
divergência aparece na primeira requisição como
Source model X does not match query rules model Y.
A nulabilidade da relação tem de bater com a derivação, que é
relation.isNullable || relation.isOneToMany: uma coleção OneToMany é sempre
nullable: true, e um ManyToOne sobre uma chave estrangeira NOT NULL tem de
ser nullable: false.
O que o compilador faz
- Junções idempotentes para filtro,
search,sortefields, mesmo semincludes; joins de predicado ficam separados dos de apresentação. - Chaves primárias compostas, inclusive no desempate de paginação sobre todas as partes.
- Paginação em duas fases quando a projeção inclui relação
many, então uma página nunca é inflada pelo join. EXISTScorrelacionado — com correlação por chave estrangeira composta — para filtro ou alvo desearchque atravesse uma relaçãomany, em qualquer profundidade. Ototalcontinua contando roots e a página continua com o tamanho pedido.
Cadeias existenciais, em qualquer profundidade
Uma condição existencial — filtro ou alvo de search cujo path atravessa
uma relação many — compila com o que vier depois daquele salto: outra relação
one (posts.author.name), uma segunda coleção, ou uma many-to-many
(posts.tags.label). Nenhuma forma é recusada, e o adapter não emite
CAPABILITY_UNAVAILABLE em lugar nenhum.
A forma compilada é sempre um EXISTS correlacionado ao root uma só
vez, no primeiro salto; cada salto seguinte é um INNER JOIN dentro da
subconsulta, e a condição da folha é qualificada pelo último alias.
Correlacionar cada salto por fora traria a coleção para o FROM externo,
inflaria os roots e estragaria o total — exatamente o problema que o EXISTS
existe para evitar.
Na many-to-many a tabela de junção é um passo como os outros, então é ela que
fica no FROM da subconsulta, e a tabela alvo entra por join a partir dela.
Esses identificadores de junção são os únicos que este compilador tira da
estratégia de nomes do TypeORM em vez das suas entidades (articlesId), então
passam pelo escape do driver.
As regras que você pode declarar também não ficam limitadas — era ali que o
limite mordia primeiro, na subida, e ele acabou: search: ['items.company.name']
sobe e roda, que é o que o
02-app-with-postgres
declara.
ESM: @nestjs/typeorm 12
O @nestjs/typeorm@12 é "type": "module" sem entrada CommonJS, o que mata os
globs baseados em __dirname que toda aplicação NestJS + TypeORM tem:
- entities: [path.join(__dirname, '/../**/*.entity{.ts,.js}')],
- migrations: [path.join(__dirname, '/migrations/*{.ts,.js}')],
+ entities: [User, Company, AccessRequest],
+ migrations: MIGRATIONS, // um array explícito e ordenado__dirname não existe em ESM, então sob runtime ESM a aplicação sobe com zero
entidades e zero migrations — sem erro, apenas um schema vazio. Listar
explicitamente tem um segundo benefício: uma entidade renomeada quebra o build
em vez da subida.
Entidades num ciclo de relação também querem o próprio Relation<T> do
TypeORM; sem ele o emitDecoratorMetadata avalia uma classe ainda em TDZ e a
inicialização estoura com Cannot access 'Category' before initialization.
O customize recebe o SelectQueryBuilder
No TypeORM o contexto nativo entregue ao customize é o SelectQueryBuilder
tipado, uma vez por query do escopo:
await this.queryBuilderService.execute(
typeormSource(this.userRepository),
query,
rules,
{
customize: (qb) => {
qb.andWhere('root.tenant_id = :tenant', { tenant });
},
customizeScope: 'both',
}
);'both' é o default e o seguro — veja
Customizando a Query.
Próximos passos
- Adapter Prisma e adapter Drizzle para os mesmos conceitos por outro ângulo.
- Guia de Uso para todos os parâmetros e operadores.