Adapter Drizzle
Use o nestjs-rest-query com o Drizzle ORM - descritor lógico declarado, dialeto explícito e os limites que ele declara.
O adapter Drizzle compila o plano de query num statement explícito — aliases, junções, condições, ordem, paginação — que um executor então materializa no dialeto declarado.
Instalação
pnpm add drizzle-orm postgres
# mysql2 para MySQL, ou o driver do SQL Server0.45.x não é suportado
A faixa de peer é >=1.0.0-rc.4 <1.0.0 — fechada nos release candidates contra
os quais a matriz de paridade foi medida. Um consumidor 2.x em 0.45.x tem de
subir, e quatro coisas quebram no caminho, nenhuma delas descrita pelas release
notes do próprio Drizzle de um jeito que você conectaria a esta biblioteca:
drizzle(client, { schema })não existe mais. A assinatura da 1.x deixa sódrizzle({ client }). O argumento{ schema }servia à API relacional (db.query.*), que este adapter não usa.relations()foi removido dodrizzle-orm(substituído pordefineRelations). Na v3 a resposta certa é apagar aquelas declarações: as relações são declaradas no descritor lógico, por path pontuado.declaration: true+ TypeScript 6 +drizzle-orm1.x é TS2883. Os tipos quepgTable()edrizzle()inferem não são nomeáveis de fora do pacote. Uma aplicação não publica tipos, então use"declaration": false. OskipLibChecknão resolve este — ele continua necessário por outro motivo, porque odrizzle-orm@1.0.0-rc.4dá erro dentro do próprio.d.ctsno TypeScript 6.db.all()só existe na família SQLite. PostgreSQL, MySQL e SQL Server expõemexecute(), e cada um devolve uma forma diferente. Qual método chamar sai do dialeto que você declara, nunca da inspeção do objeto — e é por isso que o dialeto é argumento obrigatório.
O executor, uma vez
O que os seus serviços injetam não é o db do Drizzle: é o DrizzleDatabase
que drizzleDatabase() devolve.
import { Global, Module } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import {
drizzleDatabase,
type DrizzleDatabase,
} from 'nestjs-rest-query/drizzle';
export function createDatabase(url: string) {
return drizzle({ client: postgres(url, { max: 5 }) });
}
export type AppDatabase = ReturnType<typeof createDatabase>;
export const APP_DATABASE = Symbol('APP_DATABASE');
export const DRIZZLE_EXECUTOR = Symbol('DRIZZLE_EXECUTOR');
@Global()
@Module({
providers: [
{ provide: APP_DATABASE, useFactory: () => createDatabase(databaseUrl()) },
{
provide: DRIZZLE_EXECUTOR,
inject: [APP_DATABASE],
// Sem cast: o `db` do postgres-js satisfaz DrizzleClientLike
// estruturalmente, porque expõe execute().
useFactory: (client: AppDatabase): DrizzleDatabase =>
drizzleDatabase({ client, dialect: 'postgres' }),
},
],
exports: [APP_DATABASE, DRIZZLE_EXECUTOR],
})
export class DatabaseModule {}O descritor lógico
O adapter não inspeciona o seu pgTable. Você declara um descritor lógico, e
é esse descritor que decide tipo do valor, nulabilidade, quais colunas são
internas, qual coluna dobrada apoia search/ilike e qual coluna dá ordem
total portável.
import {
createDrizzleTable,
type DrizzleRelationMap,
type DrizzleTable,
} from 'nestjs-rest-query/drizzle';
export const companiesTable: DrizzleTable = createDrizzleTable({
name: 'companies',
model: 'company',
columns: {
id: {
name: 'id',
kind: 'uuid',
nullable: false,
primaryKey: true,
// Sem isto, toda requisição falha: o desempate da paginação é sempre
// aplicado sobre a chave primária, e uuid não tem ordem total portável.
portableOrderField: 'idOrder',
},
idOrder: {
name: 'idOrder',
kind: 'string',
nullable: false,
primaryKey: false,
internal: true,
},
name: {
name: 'name',
kind: 'string',
nullable: false,
primaryKey: false,
foldedField: 'nameFolded',
},
nameFolded: {
name: 'nameFolded',
kind: 'string',
nullable: false,
primaryKey: false,
internal: true,
},
createdAt: {
name: 'createdAt',
kind: 'datetime',
nullable: false,
primaryKey: false,
},
},
});O kind é a decisão mais consequente aqui. uuid não é string: ele proíbe
gt/lt/between sem portableOrderField e recusa valor que não seja um UUID
canônico, em vez de comparar texto arbitrário.
Relações, por path pontuado
export const userRelations: DrizzleRelationMap = {
company: {
target: companiesTable,
cardinality: 'one',
// companyId é nullable, então a relação também é: o LEFT JOIN sem
// correspondência vira company: null, não um objeto cheio de nulos.
nullable: true,
sourceColumn: 'companyId',
targetColumn: 'id',
},
posts: {
target: postsTable,
cardinality: 'many',
nullable: true,
sourceColumn: 'id',
targetColumn: 'userId',
},
};A chave é o path visto do root: company é um salto, company.owner seria o
seguinte. Só as chaves sem ponto entram no schema lógico do root; as pontuadas
existem para o compilador saber juntar ou correlacionar caminhos profundos.
sourceColumn/targetColumn são as colunas da junção na direção do path, e
não "a FK de verdade": numa relação many o lado do root entra com id e o
alvo com userId, o inverso de uma relação one.
Monte o registry a partir do descritor
import { defineQueryRules } from 'nestjs-rest-query';
import type { SchemaRegistry } from 'nestjs-rest-query';
import { buildSourceSchema } from 'nestjs-rest-query/drizzle';
export const USER_SCHEMAS: SchemaRegistry = new Map([
['user', buildSourceSchema(usersTable, userRelations)],
['company', buildSourceSchema(companiesTable, {})],
['post', buildSourceSchema(postsTable, {})],
]);O buildSourceSchema é a função que o drizzleSource usa internamente para
descrever a source, então derivar o registry dela remove uma classe inteira de
SOURCE_CONFIGURATION_INVALID: escrever um defineQuerySchema à mão aqui daria
duas verdades sobre a mesma tabela, e o núcleo compara as duas antes de
executar.
A source, por requisição
import { drizzleSource, type DrizzleDatabase } from 'nestjs-rest-query/drizzle';
async findAll(
query: DynamicQueryDto,
rules: CompiledQueryRules,
): Promise<NormalizedQueryResult<object>> {
return this.queryBuilderService.execute(
drizzleSource({
db: this.db, // o DrizzleDatabase, não o db cru do drizzle
dialect: 'postgres', // tem de bater com o executor; falha fechado
table: usersTable,
relations: userRelations,
}),
query,
rules,
);
}O dialect declarado tem de ser o do executor. Divergir não daria erro em lugar
nenhum — daria resultado errado, porque paginação e coerção de boolean saem
do dialeto —, então o drizzleSource compara os dois e falha fechado.
O drizzleSource<TRow> é genérico no tipo da linha, e o TRow chega pelo
executor: monte-o como drizzleDatabase<UserRow>({ client, dialect }) e o
drizzleSource({ db, ... }) infere UserRow, então o método pode ser anotado
Promise<NormalizedQueryResult<UserRow>> sem cast. O object acima é só o
default de quem não estreita o tipo.
O que o compilador faz
Relações por path pontuado, planner de junções idempotente, EXISTS
correlacionado para qualquer salto many — inclusive cadeias, com o segundo
salto como join dentro da subconsulta — e coleção de primeiro nível
hidratada por consulta própria. ILIKE nunca é emitido.
A chave lógica e a coluna física são coisas diferentes, e as duas são
honradas. A chave de columns é o campo lógico — o que o fields=, as regras
e o JSON falam — e o DrizzleColumn.name é a coluna física que o compilador
põe no SQL por sql.identifier(...). Uma chave de descritor diferente do nome
físico da coluna é mapeamento suportado, não defeito.
Campo não declarado falha fechado enquanto a aplicação sobe, não no banco: o
buildSourceSchema passa o sourceColumn e o targetColumn de cada relação
pela mesma resolução, então coluna de junção não declarada é
SOURCE_CONFIGURATION_INVALID —
Drizzle table users has no column declared for field companyId — na
construção da source.
O
03-app-with-drizzle
usa o mapeamento: nomes físicos em snake_case (id_order, name_folded,
created_at) atrás de chaves lógicas em camelCase, com uma verificação na
subida contra getTableColumns(pgTable) de que cada chave é a propriedade do
pgTable e cada name é a coluna física daquela propriedade.
Limites
Coleção aninhada sob outra relação falha fechado. Projetar uma relação
many pendurada em outra relação é recusado com ADAPTER_CONTRACT_VIOLATION —
Drizzle adapter cannot project the nested to-many relation <path> — em vez de
devolver silenciosamente uma coleção vazia. Coleções de primeiro nível são
suportadas.
drizzle-kit push/generate não expressam o perfil certificado. Eles não
emitem COLLATE "C", e collation por ponto de código nas colunas de texto
portáveis faz parte da promessa de paridade. O exemplo 03 emite DDL explícito em
src/database/bootstrap.ts;
veja
test/profiles/
para o DDL de referência por família.
Migrando uma source Drizzle da 2.x
A v2 entregava objetos de coluna do Drizzle ao adapter; a v3 recebe um descritor lógico e nomes simples:
| v2 | v3 |
|---|---|
source.db = o db do Drizzle | drizzleDatabase({ client: db, dialect }) |
source.table = um pgTable | createDrizzleTable({ name, model, columns }) (um descritor) |
source.primaryKey = uma coluna | columns[x].primaryKey: true |
relations.x.table | relations.x.target (outro descritor) |
relations.x.on: eq(a, b) | relations.x.sourceColumn + .targetColumn |
relations.x.primaryKey (era obrigatório) | removido |
| — | relations.x.nullable (obrigatório) |
| — | relations['x.y'] para saltos profundos |
O columnMap também deixou de existir: os paths resolvem pelo descritor lógico.
Próximos passos
- Guia de Uso para todos os parâmetros, operadores e códigos de erro.
- Visão geral dos adapters para a tabela completa de divergências.
- O guia de migração completo
para o caminho
2.x→3.x.