import type { PartialMessage } from "@bufbuild/protobuf"; import { z } from "zod"; import type { Property } from "@superblocksteam/types/dist/src/common/v1/plugin_pb"; import type { Plugin as RestApiIntegrationPlugin } from "@superblocksteam/types/dist/src/plugins/restapiintegration/v1/plugin_pb"; import { RestApiValidationError } from "../../errors.js"; import type { QueryExecutor, TraceMetadata } from "../registry.js"; import type { IntegrationConfig, IntegrationClientImpl } from "../types.js"; import { decodeWorkerBinaryResponse } from "./decode-worker-binary-response.js"; import { REST_API_RESPONSE_TYPES } from "./types.js"; import type { ApiRequestOptions } from "./types.js"; export type RestApiRequest = PartialMessage; export abstract class RestApiClientBase implements IntegrationClientImpl { readonly name: string; readonly pluginId: string; readonly config: IntegrationConfig; protected readonly executeQuery: QueryExecutor; constructor(config: IntegrationConfig, executeQuery: QueryExecutor) { this.name = config.name; this.pluginId = config.pluginId; this.config = config; this.executeQuery = executeQuery; } /** * Create a Property object for query params / headers. */ protected createParam(key: string, value: unknown): PartialMessage { return { key, value: typeof value === "string" ? value : JSON.stringify(value), }; } protected async executeApiRequest( options: ApiRequestOptions, bodySchema?: z.ZodSchema, metadata?: TraceMetadata, ): Promise { if (options.body !== undefined && bodySchema) { const bodyParseResult = bodySchema.safeParse(options.body); if (!bodyParseResult.success) { throw new RestApiValidationError( `Request body validation failed: ${bodyParseResult.error.message}`, { zodError: bodyParseResult.error, data: options.body, }, ); } } const headers: PartialMessage[] = []; if (options.headers) { for (const [key, value] of Object.entries(options.headers)) { headers.push(this.createParam(key, value)); } } const params: PartialMessage[] = []; if (options.params) { for (const [key, value] of Object.entries(options.params)) { params.push(this.createParam(key, value)); } } const responseTypeResult = z .enum(REST_API_RESPONSE_TYPES) .safeParse(options.responseType ?? "json"); if (!responseTypeResult.success) { throw new RestApiValidationError( `Unsupported responseType ${JSON.stringify(options.responseType)} - expected one of: ${REST_API_RESPONSE_TYPES.join(", ")}`, { zodError: responseTypeResult.error, data: options.responseType, }, ); } const responseType = responseTypeResult.data; const request: RestApiRequest = { openApiAction: "genericHttpRequest", httpMethod: options.method.toUpperCase(), urlPath: options.path, headers, params, responseType, }; if (options.body !== undefined) { request.body = JSON.stringify(options.body); request.bodyType = "jsonBody"; } const result = await this.executeQuery({ ...request }, undefined, metadata); if (result === null || result === undefined) { throw new RestApiValidationError( `Integration query returned ${String(result)} for responseType "${responseType}" - expected a response value`, { zodError: new z.ZodError([ { code: z.ZodIssueCode.custom, message: "response value is required", path: [], }, ]), data: result, }, ); } switch (responseType) { case "binary": return decodeWorkerBinaryResponse(result); case "json": case "text": return result; default: { const _exhaustive: never = responseType; return _exhaustive; } } } }