import { DataService } from "./services/dataService"; import { BranchService } from "./services/BranchService"; import { RealtimeService } from "./services/realtimeService"; import { DatabasePoolManager } from "./databasePoolManager"; import { DrizzleClient } from "./interfaces"; import { DatabaseAdmin, DataDriver, DeleteProps, CollectionConfig, FetchCollectionProps, FetchOneProps, ListenCollectionProps, ListenOneProps, RebaseClient, RebaseSdkData, RestFetchService, SaveManyProps, SaveProps, UpdateManyProps, DeleteManyProps, TableMetadata, User } from "@rebasepro/types"; import { PostgresCollectionRegistry } from "./collections/PostgresCollectionRegistry"; import { HistoryService } from "./history/HistoryService"; export declare class PostgresBackendDriver implements DataDriver { db: DrizzleClient; readonly registry: PostgresCollectionRegistry; poolManager?: DatabasePoolManager | undefined; key: string; initialised: boolean; dataService: DataService; realtimeService: RealtimeService; historyService?: HistoryService; branchService?: BranchService; user?: User; data: RebaseSdkData; client?: RebaseClient; /** * Auto-set to `true` when a SET LOCAL ROLE fails with insufficient * privileges, so subsequent queries skip the doomed attempt. * Mirrors the static `DISABLE_DB_ROLE_SWITCHING` env var but is * learned at runtime. */ private _roleSwitchingDisabled; /** * Restricted role that authenticated (user-context) requests run as (via * `SET LOCAL ROLE`) so RLS binds every statement — reads *and* writes. Set * by the bootstrapper after posture detection: defined when the connection * would otherwise bypass RLS (superuser / BYPASSRLS / table owner), * undefined when RLS already applies natively. The base (server-context) * driver never switches — it is the trusted owner plane (auth flows, * migrations, `dataAsAdmin`). */ rlsUserRole?: string; /** * When true, realtime notifications are deferred until after the * wrapping transaction commits. Set by `withAuth` → `withTransaction`. */ _deferNotifications: boolean; _pendingNotifications: Array<{ path: string; id: string; row: Record | null; databaseId?: string; }>; constructor(db: DrizzleClient, realtimeService: RealtimeService, registry: PostgresCollectionRegistry, user?: User, poolManager?: DatabasePoolManager | undefined, historyService?: HistoryService); /** * Typed admin capabilities (SQLAdmin + SchemaAdmin + BranchAdmin). * Implemented as a getter so method references are resolved at call-time, * allowing test spies applied after construction to take effect. */ get admin(): DatabaseAdmin; /** * REST-optimised fetch service (include-aware eager-loading). * Delegates to the underlying FetchService (include-aware eager loading), * then runs the afterRead pipeline on the results. The raw FetchService does * NOT run callbacks, so masking must be applied here — otherwise every * REST/SDK read leaks unmasked data (see {@link applyAfterReadForRest}). */ get restFetchService(): RestFetchService; /** * Build the context handed to every collection callback. * * Note `data: this.data` — `this` is whichever driver is running the * operation, so the callback's data plane inherits that driver's privilege. * On a user request `AuthenticatedPostgresBackendDriver.withTransaction` * constructs a fresh base driver bound to the RLS-scoped transaction and * runs the operation on it, so `this.data` speaks through that connection * and policies apply. On server-context work `this` is the base driver on * the owner connection, and they do not. Pinned by the * `"scopes context.data to the caller"` case in the `rls-enforcement` e2e * suite, because it is the kind of property that is easy to break from a * distance and impossible to notice. * * Previously returned through `as unknown as RebaseCallContext`, which * disabled checking for the whole object and let `driver` — documented in * the callbacks guide — sit on the runtime context while absent from the * contract. Both are declared now, so this is a plain typed return. */ private buildCallContext; private resolveCollectionCallbacks; /** * Run the three-tier afterRead pipeline (global → collection → property) on a * single row for a collection whose callbacks have already been resolved. */ private applyAfterReadToRow; private static hasAfterRead; /** * Apply afterRead to REST/SDK read results. * * The REST / `include` path fetches rows through the raw fetch service, which * does NOT run callbacks — so without this, `afterRead` transforms (e.g. PII * masking) are silently skipped on every SDK/REST read, leaking raw data. * This choke point guarantees afterRead runs there too, matching the driver's * fetchCollection/fetchOne paths. * * It also masks embedded relation data one level deep by running the TARGET * collection's afterRead (so `post.author.email` is masked by the authors * collection, not left raw). */ applyAfterReadForRest(rows: Record[], path: string): Promise[]>; fetchCollection>({ path, collection, filter, limit, offset, startAfter, orderBy, searchString, order, vectorSearch }: FetchCollectionProps): Promise[]>; listenCollection>({ path, collection, filter, limit, offset, startAfter, orderBy, searchString, order, onUpdate, onError }: ListenCollectionProps): () => void; fetchOne>({ path, id, databaseId, collection }: FetchOneProps): Promise | undefined>; listenOne>({ path, id, collection, onUpdate, onError }: ListenOneProps): () => void; save>({ path, id, values, collection, status, upsert }: SaveProps): Promise>; /** * Write many rows through the same pipeline as {@link save}. * * The batch runs in one transaction of its own, so a failure part-way leaves * nothing behind — the point of a batch is that a re-run starts from a known * state. When this driver is already inside a transaction (the authenticated * path, via `withTransaction`) the nested call becomes a savepoint, which is * still atomic and still commits once. * * Rows are applied in order, so a batch that touches the same key twice ends * with the last write winning, exactly as separate calls would. */ saveMany>({ path, rows, collection, upsert }: SaveManyProps): Promise[]>; /** * Update many rows through the same pipeline as {@link save}, in one * transaction. * * Structurally the mirror of {@link saveMany} — same tx-bound sub-driver, * same deferred notifications, same per-row error labelling — but it calls * `save` with an explicit `id` and `status: "existing"`, which is precisely * what `saveMany` cannot do: that one passes `status: "new"` and keeps the * key inside `values`, so it inserts or upserts and can never target a * particular row. * * All-or-nothing, so an id matching no row aborts the batch. A partial * update is the outcome with no good recovery: the caller cannot tell which * half landed without re-reading everything. */ updateMany>({ path, updates, collection }: UpdateManyProps): Promise[]>; /** * Delete many rows in one transaction, running the full delete pipeline — * `beforeDelete`, the delete, `afterDelete` — for each. * * Looping the single-row {@link delete} rather than emitting one * `DELETE ... WHERE id = ANY($1)` is the deliberate choice: a single * statement would be faster and would skip every callback, so a collection * relying on `beforeDelete` to veto or on `afterDelete` to clean up * dependents would behave differently depending on how many rows the caller * happened to delete at once. Same pipeline, one transaction. */ deleteMany>({ path, ids, collection }: DeleteManyProps): Promise; delete>({ row, collection }: DeleteProps): Promise; deleteAll(path: string): Promise; checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise; count>({ path, collection, filter, logical, searchString, vectorSearch }: FetchCollectionProps): Promise; private getTargetDb; executeSql(sqlText: string, options?: { database?: string; role?: string; params?: unknown[]; }): Promise[]>; fetchAvailableDatabases(): Promise; fetchAvailableRoles(): Promise; /** * Application-level roles actually in use in this project. * * Distinct from {@link fetchAvailableRoles}, which returns native * PostgreSQL roles from `pg_roles` (`postgres`, `rebase_user`, …). Those * are the roles the SQL editor can `SET ROLE` to. *These* are the strings * held in the users table's `roles` column, injected per-transaction as * `rebase.roles()` and matched by `SecurityRule.roles`. Feeding the pg roles * into a `SecurityRule.roles` field produces a condition no user can ever * satisfy, so the two must not be conflated. * * Roles have no registry table — they were migrated out of * `rebase.user_roles` onto an inline `roles TEXT[]` column — so the live * set is derived from what is assigned. A role that is declared in a policy * but held by nobody yet cannot be discovered here; callers that need it * should union in the roles they already know about. */ fetchApplicationRoles(): Promise; fetchCurrentDatabase(): Promise; /** * Fetch public tables that are not yet mapped to a collection. * Excludes internal tables (_rebase_*, _auth_*, auth tables, etc.) * and junction/connection tables used for many-to-many relations. */ fetchUnmappedTables(mappedPaths?: string[]): Promise; /** * Fetch metadata for a given table from information_schema (columns, policies, constraints). */ fetchTableMetadata(tableName: string): Promise; private generateSubscriptionId; /** * Create a new delegate instance with authenticated context. * Starts a transaction and sets the current_user_id and current_user_roles * configuration parameters for PostgreSQL Row Level Security. */ withAuth(user: User): Promise; } export declare class AuthenticatedPostgresBackendDriver implements DataDriver { delegate: PostgresBackendDriver; key: string; initialised: boolean; user: User; data: RebaseSdkData; constructor(delegate: PostgresBackendDriver, user: User); /** * Typed admin capabilities — delegates to the base driver. */ admin: DatabaseAdmin; get restFetchService(): RestFetchService; private withTransaction; fetchCollection>(props: FetchCollectionProps): Promise[]>; /** * Injects the authenticated user's context into the most recently * registered realtime subscription so RLS-aware polling can apply. */ private injectAuthContext; listenCollection>(props: ListenCollectionProps): () => void; fetchOne>(props: FetchOneProps): Promise | undefined>; listenOne>(props: ListenOneProps): () => void; save>(props: SaveProps): Promise>; /** * One transaction for the whole batch, rather than one per row. * * This is the point of the method: `save` opens a transaction per call, so * importing 10k rows through it means 10k transactions (and, over HTTP, 10k * round trips). Here the RLS context is established once and every row lands * or none does. Realtime notifications are already deferred to commit by * `withTransaction`, so a batch does not flood subscribers mid-flight. */ saveMany>(props: SaveManyProps): Promise[]>; delete>(props: DeleteProps): Promise; deleteAll(path: string): Promise; checkUniqueField(path: string, name: string, value: unknown, id?: string, collection?: CollectionConfig): Promise; count>(props: FetchCollectionProps): Promise; }