UNPKG

@accounter/server

Version:
115 lines (114 loc) 4.77 kB
import type { PoolClient, QueryResult, QueryResultRow } from 'pg'; import { AuthContextProvider } from '../auth/providers/auth-context.provider.js'; import { DBProvider } from './db.provider.js'; export declare function isDataModifyingQuery(text: string): boolean; /** * TenantAwareDBClient enforces Row-Level Security (RLS) by setting PostgreSQL * session variables on a request-scoped transaction. * * RLS Enforcement: * - app.current_business_id: Set to the authenticated user's active business * - app.current_user_id: Set to the authenticated user's ID (or NULL for API keys) * - app.auth_type: Set to 'jwt' or 'apiKey' * * **Usage:** * Inject into Operation-scoped providers via constructor DI: * * @example * @Injectable({ scope: Scope.Operation }) * class BusinessesProvider { * constructor(private db: TenantAwareDBClient) {} * * async getBusinesses() { * return this.db.query('SELECT * FROM businesses') * } * } * * Session model (request-scoped): * - The first query checks out one pooled connection and opens a transaction * with the RLS variables set once. Subsequent read queries reuse it — one * round trip per query instead of BEGIN/SET/query/COMMIT for each. * - Data-modifying stand-alone queries and explicit `transaction()` scopes are * committed immediately on success, so a mutation response always reflects * durable state. The read session re-opens lazily on the next query. * - A failed statement aborts the surrounding transaction (Postgres 25P02), so * errors roll the session back and the next query starts a fresh one. Only * uncommitted read-only work is discarded — writes were already committed. * - `dispose()` (invoked by dbCleanupPlugin at request/stream end) commits any * open read session and releases the connection back to the pool. * * Transaction Management: * - Supports nested transactions via SAVEPOINTs * - Automatically rolls back on error * - Automatically releases connection on dispose * * **DO NOT** access from Yoga context - use DI injection instead. * * @throws {GraphQLError} UNAUTHENTICATED if auth context is null */ export declare class TenantAwareDBClient { private dbProvider; private authContextProvider; private mutex; private storage; private activeClient; private sessionOpen; private transactionDepth; private isDisposed; private authContext; private authContextInitialized; /** * Per-operation mode: commit and release the connection after every * top-level query/transaction (the pre-request-scoped behavior). Defaults to * true for direct constructions outside the GraphQL request lifecycle (no * CONTEXT injection — test harnesses, scripts) where nothing calls * dispose(): a held connection would otherwise leak from the pool, keep * table locks, and block pool.end(). */ autoRelease: boolean; constructor(dbProvider: DBProvider, authContextProvider: AuthContextProvider, context?: GraphQLModules.GlobalContext); /** * Execute a query with RLS enforcement on the request-scoped session. * Data-modifying statements are committed immediately. */ query<T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]): Promise<QueryResult<T> & { rowCount: number; }>; /** * Execute a function within a transaction block. * Handles nested transactions using SAVEPOINTs. The outermost scope is * committed immediately on success. */ transaction<T>(fn: (client: PoolClient) => Promise<T>): Promise<T>; private executeTransactionInternal; /** * Ensure the request-scoped session is open: one pooled connection for the * whole request, with an open transaction carrying the RLS variables. * Always called while holding the mutex. */ private ensureSession; /** * Close the open transaction (COMMIT or ROLLBACK). The connection is kept * for the next session unless the close itself fails, in which case the * connection state is unknown and it is destroyed. */ private endSession; private releaseClient; /** * Set PostgreSQL session variables for Row-Level Security. */ private setRLSVariables; /** * End-of-request cleanup: commit any open read session and release the * connection. Invoked by dbCleanupPlugin once the response (including any * deferred stream) is fully sent; safe to call manually for direct * constructions. */ dispose(): Promise<void>; private ensureNotDisposed; /** * Lazy initialization of auth context on first use. * This ensures the async provider is called only when needed. */ private ensureAuthContext; }