@accounter/server
Version:
Accounter GraphQL server
115 lines (114 loc) • 4.77 kB
TypeScript
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;
}