/** * PostgreSQL client implementation. * * Uses the proto-generated Plugin type from @superblocksteam/types * for type-safe request building. */ import type { PartialMessage } from "@bufbuild/protobuf"; import type { z } from "zod"; import type { Plugin as PostgresPlugin } from "@superblocksteam/types/dist/src/plugins/postgresql/v1/plugin_pb"; import { QueryValidationError } from "../../errors.js"; import { IntegrationError } from "../../runtime/errors.js"; import type { QueryExecutor, TraceMetadata } from "../registry.js"; import type { IntegrationConfig, IntegrationClientImpl } from "../types.js"; import { describeType } from "../utils.js"; import type { PostgresClient } from "./types.js"; /** * PostgreSQL request type derived from proto definition. * Using PartialMessage allows optional fields. */ type PostgresRequest = PartialMessage; /** * Internal implementation of PostgresClient. * * This implementation communicates with the orchestrator to execute queries. * At runtime, the orchestrator injects the actual database connection and * handles authentication. */ export class PostgresClientImpl implements PostgresClient, IntegrationClientImpl { readonly name: string; readonly pluginId: string; readonly config: IntegrationConfig; private readonly executeQuery: QueryExecutor; constructor(config: IntegrationConfig, executeQuery: QueryExecutor) { this.name = config.name; this.pluginId = config.pluginId; this.config = config; this.executeQuery = executeQuery; } /** * Builds a PostgreSQL plugin request object. * * When params are provided, the SQL body is sent with placeholders intact * (e.g. $1, $2) and the parameter values are passed via the `parameters` * field as a JSON-stringified array. The server performs the actual * parameter binding, avoiding client-side SQL interpolation. * * @param sql - The SQL query to execute (with $1, $2 placeholders when parameterized) * @param params - Optional query parameters for server-side binding * @returns The plugin request object matching the proto schema */ private buildRequest(sql: string, params?: unknown[]): PostgresRequest { const hasParams = params && params.length > 0; return { body: sql, parameters: hasParams ? JSON.stringify(params) : undefined, }; } async query( sql: string, schema: z.ZodSchema, params?: unknown[], metadata?: TraceMetadata, ): Promise { const request = this.buildRequest(sql, params); try { const result = await this.executeQuery(request, undefined, metadata); if (!Array.isArray(result)) { throw new IntegrationError( this.config.name, "query", `Expected array result from Postgres query, got: ${describeType(result)}`, ); } const validated: T[] = []; for (let i = 0; i < result.length; i++) { const row = result[i]; const parseResult = schema.safeParse(row); if (!parseResult.success) { throw new QueryValidationError( `Row ${i} failed validation: ${parseResult.error.message}`, { rowIndex: i, errors: parseResult.error.errors, row, }, ); } validated.push(parseResult.data); } return validated; } catch (error) { if ( error instanceof QueryValidationError || error instanceof IntegrationError ) { throw error; } throw new IntegrationError(this.config.name, "query", error); } } async execute( sql: string, params?: unknown[], metadata?: TraceMetadata, ): Promise<{ rowCount: number }> { const request = this.buildRequest(sql, params); try { const result = await this.executeQuery(request, undefined, metadata); if ( typeof result === "object" && result !== null && "rowCount" in result ) { return { rowCount: Number(result.rowCount) || 0 }; } if (Array.isArray(result)) { return { rowCount: result.length }; } return { rowCount: 0 }; } catch (error) { throw new IntegrationError(this.config.name, "execute", error); } } }