/** * GraphQL integration client base class. * * Provides the foundation for GraphQL integration clients with query() and mutation() methods. * Handles proto message construction and response validation. */ import type { PartialMessage } from "@bufbuild/protobuf"; import { z } from "zod"; import type { Property } from "@superblocksteam/types/dist/src/common/v1/plugin_pb"; import { RestApiValidationError } from "../../errors.js"; import type { QueryExecutor, TraceMetadata } from "../registry.js"; import type { IntegrationConfig, IntegrationClientImpl } from "../types.js"; /** * Base class for GraphQL integration clients. * * This class handles: * - Building graphql.v1.Plugin proto messages * - Executing queries and mutations through the orchestrator * - Required Zod schema validation on full GraphQL response * - Variables serialization for parameterized queries * - Optional per-request HTTP headers (in addition to any headers configured * on the integration) */ export abstract class GraphQLIntegrationClient implements IntegrationClientImpl { readonly name: string; readonly pluginId: string; readonly config: IntegrationConfig; private executeQuery: QueryExecutor; constructor(config: IntegrationConfig, executeQuery: QueryExecutor) { this.name = config.name; this.pluginId = config.pluginId; this.config = config; this.executeQuery = executeQuery; } /** * Execute a GraphQL query. * * @param query - GraphQL query string * @param schema - Zod schema for full response validation (REQUIRED) * @param variables - Optional variables for the query * @param metadata - Optional trace metadata for observability * @param headers - Optional HTTP headers to send with the request * @returns Validated query result */ async query( query: string, schema: { response: z.ZodSchema }, variables?: Record, metadata?: TraceMetadata, headers?: Record, ): Promise { return this.executeGraphQL(query, schema, variables, metadata, headers); } /** * Execute a GraphQL mutation. * * @param mutation - GraphQL mutation string * @param schema - Zod schema for full response validation (REQUIRED) * @param variables - Optional variables for the mutation * @param metadata - Optional trace metadata for observability * @param headers - Optional HTTP headers to send with the request * @returns Validated mutation result */ async mutation( mutation: string, schema: { response: z.ZodSchema }, variables?: Record, metadata?: TraceMetadata, headers?: Record, ): Promise { return this.executeGraphQL(mutation, schema, variables, metadata, headers); } /** * Execute a GraphQL operation (query or mutation). * * Builds the graphql.v1.Plugin proto message, executes through orchestrator, * and validates the full response against the required schema. */ private async executeGraphQL( queryString: string, schema: { response: z.ZodSchema }, variables?: Record, metadata?: TraceMetadata, headers?: Record, ): Promise { const headerProps: PartialMessage[] = []; if (headers) { for (const [key, value] of Object.entries(headers)) { headerProps.push({ key, value }); } } // Build graphql.v1.Plugin proto message const request: Record = { body: queryString, custom: variables ? { variables: { key: "variables", value: JSON.stringify(variables), }, } : undefined, verboseHttpOutput: false, failOnGraphqlErrors: true, }; if (headerProps.length > 0) { request.headers = headerProps; } // Execute query through orchestrator const response = await this.executeQuery(request, undefined, metadata); // Return full GraphQL response without unwrapping // Users should access result.data themselves // GraphQL responses have shape: { data: {...}, errors?: [...] } // Schema is REQUIRED - always validate const result = schema.response.safeParse(response); if (!result.success) { throw new RestApiValidationError("GraphQL response validation failed", { zodError: result.error, data: response, }); } return result.data; } }