Skip to content

NestJS

uql-orm/nestjs registers your querier pool with Nest’s DI container, sets it as UQL’s default pool (so middleware, getQuerier() and @Transactional keep working), and ends it on application shutdown.

app.module.ts
import { Module } from '@nestjs/common';
import { UqlModule } from 'uql-orm/nestjs';
import { pool } from './uql.config.js';
@Module({
imports: [UqlModule.forRoot({ pool })], // global by default; pass global: false to scope it
})
export class AppModule {}

forRootAsync builds the pool from other providers, ConfigService from @nestjs/config being the usual one:

import { PgQuerierPool } from 'uql-orm/postgres';
import { ConfigModule, ConfigService } from '@nestjs/config';
UqlModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => new PgQuerierPool({ connectionString: config.get('DATABASE_URL') }),
});
import { Inject, Injectable } from '@nestjs/common';
import { UQL_QUERIER_POOL } from 'uql-orm/nestjs';
import type { QuerierPool, Query, UniversalQuerier } from 'uql-orm/type';
import { User } from './shared/models/index.js';
@Injectable()
export class UsersService {
constructor(@Inject(UQL_QUERIER_POOL) private readonly pool: QuerierPool) {}
findMany(q: Query<User>) {
return this.pool.findMany(User, q);
}
create(user: User, db: UniversalQuerier = this.pool) {
return db.insertOne(User, user);
}
}

The pool is the stateless, shareable resource, which is why it is the thing to own via DI. A Querier holds a connection and possibly an open transaction: as a singleton it would pin one connection for the app’s lifetime and share transaction state across requests; request-scoped, it would re-instantiate the whole provider graph per request.

Accepting a UniversalQuerier that defaults to the pool is what lets two services share one commit, with no request-scoped providers and no interceptor owning the release:

await this.pool.transaction(async (querier) => {
await this.users.create(user, querier);
await this.audit.record({ type: 'user.created' }, querier);
});

UQL_QUERIER_POOL is for your own providers. UQL’s internals read the default pool that forRoot registers, so overriding the DI provider does not redirect them.

Pass getContext and UQL wires a global interceptor that runs every request inside withContext, so security filters apply to every query, relations, cascades and @Transactional services included:

forRoot is generic in your request type, so name the shape getContext reads and req is typed inside it:

type AuthedRequest = { user: { id: string; tenantId: number } };
UqlModule.forRoot<AuthedRequest>({
pool,
// derive from the verified request (session / JWT), never from client input
getContext: (req) => ({ tenantId: req.user.tenantId, userId: req.user.id }),
});
import { Entity, Filter } from 'uql-orm';
@Filter('tenant', {
condition: (ctx) => (ctx?.tenantId != null ? { companyId: ctx.tenantId } : undefined),
security: true,
})
@Entity()
export class Invoice {}
this.pool.findMany(Invoice, {}); // ... WHERE companyId = <ctx.tenantId>

An interceptor runs after guards, so req.user is populated, which is what you want for tenant-from-JWT. The context reaches controllers, services and @Transactional methods, but not guards or exception filters. If you derive it from a header or sub-domain, mount your own middleware instead: withContext(getContext(req), () => next()).

Nest’s default platform is Express, so the Express middleware mounts in main.ts:

const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // lets UqlModule end the pool on SIGTERM
app.use('/api', querierMiddleware({ include: [User, Post] }));
await app.listen(3000);

Unknown routes fall through, so hand-written controllers coexist under the same prefix. On the Fastify platform, use the createRequestHandler bridge.