NestJS
NestJS Extension
Section titled “NestJS Extension”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.
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') }),});Injecting the pool
Section titled “Injecting the pool”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.
Multi-tenancy
Section titled “Multi-tenancy”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()).
Auto-generated entity routes
Section titled “Auto-generated entity routes”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 SIGTERMapp.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.