/** * BigQuery integration client implementation. * * Executes BigQuery queries through the Superblocks orchestrator with * runtime validation using Zod schemas. */ import type { PartialMessage } from "@bufbuild/protobuf"; import type { z } from "zod"; import type { Plugin as BigQueryPlugin } from "@superblocksteam/types/dist/src/plugins/bigquery/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 { BigQueryClient } from "./types.js"; /** * Internal implementation of the BigQuery client. */ export class BigQueryClientImpl implements BigQueryClient, IntegrationClientImpl { readonly config: IntegrationConfig; private readonly executeQuery: QueryExecutor; constructor(config: IntegrationConfig, executeQuery: QueryExecutor) { this.config = config; this.executeQuery = executeQuery; } get name(): string { return this.config.name; } get pluginId(): string { return this.config.pluginId; } /** * Builds a BigQuery request object from SQL and parameters. * * When params are provided, the SQL body is sent with placeholders intact * and the parameter values are passed via the `parameters` field * as a JSON-stringified array for server-side binding. * * @param sql - The SQL query with optional placeholders * @param params - Optional array of parameter values for server-side binding * @returns The plugin request object */ private buildRequest( sql: string, params?: unknown[], ): PartialMessage { 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 BigQuery 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} validation failed: ${parseResult.error.message}`, { rowIndex: i, errors: parseResult.error.errors.map((err) => ({ path: err.path, message: err.message, })), 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 }; } return { rowCount: 0 }; } catch (error) { throw new IntegrationError(this.config.name, "execute", error); } } }