253 lines · 8.8 KB · TypeScript
RawDownload
import { Effect, Layer, Option, Schema } from "effect";import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"; import type { PluginCtx, StorageFailure } from "@executor-js/sdk/core"; import { GoogleDiscoveryInvocationError, GoogleDiscoveryOAuthError } from "./errors";import type { GoogleDiscoveryStore } from "./binding-store";import {  GoogleDiscoveryInvocationResult,  type GoogleDiscoveryParameter,  type GoogleDiscoveryStoredSourceData,} from "./types"; const SAFE_METHODS = new Set(["get", "head", "options"]); const UnknownErrorMessage = Schema.Struct({ message: Schema.String });const decodeUnknownErrorMessage = Schema.decodeUnknownOption(UnknownErrorMessage); const errorMessageFromUnknown = (cause: unknown): string => {  const decoded = decodeUnknownErrorMessage(cause);  if (Option.isSome(decoded)) return decoded.value.message;  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: preserves existing fallback text for HTTP client errors  return String(cause);}; export const annotationsForOperation = (  method: string,  pathTemplate: string,): { requiresApproval?: boolean; approvalDescription?: string } => {  if (SAFE_METHODS.has(method.toLowerCase())) return {};  return {    requiresApproval: true,    approvalDescription: `${method.toUpperCase()} ${pathTemplate}`,  };}; // ---------------------------------------------------------------------------// Path / query parameter helpers (unchanged from the old invoker)// --------------------------------------------------------------------------- const stringValuesFromParameter = (value: unknown, repeated: boolean): string[] => {  if (value === undefined || value === null) return [];  if (Array.isArray(value)) {    const normalized = value.flatMap((entry) =>      entry === undefined || entry === null ? [] : [String(entry)],    );    return repeated ? normalized : [normalized.join(",")];  }  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {    return [String(value)];  }  return [JSON.stringify(value)];}; const replacePathParameters = (input: {  pathTemplate: string;  args: Record<string, unknown>;  parameters: readonly GoogleDiscoveryParameter[];}): Effect.Effect<string, GoogleDiscoveryInvocationError> =>  Effect.gen(function* () {    let failure: GoogleDiscoveryInvocationError | undefined;    const resolved = input.pathTemplate.replaceAll(/\{([^}]+)\}/g, (_, name: string) => {      const parameter = input.parameters.find(        (entry) => entry.location === "path" && entry.name === name,      );      const values = stringValuesFromParameter(input.args[name], false);      if (values.length === 0) {        if (parameter?.required) {          failure = new GoogleDiscoveryInvocationError({            message: `Missing required path parameter: ${name}`,            statusCode: Option.none(),          });        }        return "";      }      return encodeURIComponent(values[0]!);    });    if (failure) return yield* failure;    return resolved;  }); const resolveBaseUrl = (source: GoogleDiscoveryStoredSourceData): string =>  new URL(source.servicePath || "", source.rootUrl).toString(); const isJsonContentType = (contentType: string | null | undefined): boolean => {  if (!contentType) return false;  const normalized = contentType.split(";")[0]?.trim().toLowerCase() ?? "";  return (    normalized === "application/json" || normalized.includes("+json") || normalized.includes("json")  );}; // ---------------------------------------------------------------------------// HTTP request builder / executor// --------------------------------------------------------------------------- const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: {  method: string;  pathTemplate: string;  parameters: readonly GoogleDiscoveryParameter[];  hasBody: boolean;  source: GoogleDiscoveryStoredSourceData;  args: Record<string, unknown>;  authorizationHeader?: string;}) {  const client = yield* HttpClient.HttpClient;   const resolvedPath = yield* replacePathParameters({    pathTemplate: input.pathTemplate,    args: input.args,    parameters: input.parameters,  });  const requestUrl = new URL(resolvedPath.replace(/^\//, ""), resolveBaseUrl(input.source));   for (const parameter of input.parameters) {    if (parameter.location === "path") continue;    const values = stringValuesFromParameter(input.args[parameter.name], parameter.repeated);    if (values.length === 0) {      if (parameter.required) {        return yield* new GoogleDiscoveryInvocationError({          message: `Missing required ${parameter.location} parameter: ${parameter.name}`,          statusCode: Option.none(),        });      }      continue;    }    if (parameter.location === "query") {      for (const value of values) {        requestUrl.searchParams.append(parameter.name, value);      }    }  }   let request = HttpClientRequest.make(input.method.toUpperCase() as "GET")(requestUrl.toString());   for (const parameter of input.parameters) {    if (parameter.location !== "header") continue;    const values = stringValuesFromParameter(input.args[parameter.name], parameter.repeated);    if (values.length === 0) continue;    request = HttpClientRequest.setHeader(      request,      parameter.name,      parameter.repeated ? values.join(",") : values[0]!,    );  }   if (input.authorizationHeader) {    request = HttpClientRequest.setHeader(request, "Authorization", input.authorizationHeader);  }   if (input.hasBody && input.args.body !== undefined) {    request = HttpClientRequest.bodyJsonUnsafe(request, input.args.body);  }   const response = yield* client.execute(request).pipe(    Effect.mapError(      (err) =>        new GoogleDiscoveryInvocationError({          message: `HTTP request failed: ${errorMessageFromUnknown(err)}`,          statusCode: Option.none(),          cause: err,        }),    ),  );   const contentType = response.headers["content-type"] ?? null;  const mapBodyError = Effect.mapError(    (err: unknown) =>      new GoogleDiscoveryInvocationError({        message: `Failed to read response body: ${errorMessageFromUnknown(err)}`,        statusCode: Option.some(response.status),        cause: err,      }),  );  const body =    response.status === 204      ? null      : isJsonContentType(contentType)        ? yield* response.json.pipe(            Effect.catch(() => response.text),            mapBodyError,          )        : yield* response.text.pipe(mapBodyError);   const ok = response.status >= 200 && response.status < 300;  return new GoogleDiscoveryInvocationResult({    status: response.status,    headers: { ...response.headers },    data: ok ? body : null,    error: ok ? null : body,  });}); // ---------------------------------------------------------------------------// Entry point — called from plugin.invokeTool.// --------------------------------------------------------------------------- export const invokeGoogleDiscoveryTool = (input: {  ctx: PluginCtx<GoogleDiscoveryStore>;  toolId: string;  /** Resolved owning scope of the tool row. */  toolScope: string;  args: unknown;  httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;}): Effect.Effect<  GoogleDiscoveryInvocationResult,  GoogleDiscoveryInvocationError | GoogleDiscoveryOAuthError | StorageFailure> =>  Effect.gen(function* () {    const entry = yield* input.ctx.storage.getBinding(input.toolId, input.toolScope);    if (!entry) {      return yield* new GoogleDiscoveryInvocationError({        message: `No Google Discovery operation found for tool "${input.toolId}"`,        statusCode: Option.none(),      });    }    const stored = yield* input.ctx.storage.getSource(entry.namespace, input.toolScope);    if (!stored) {      return yield* new GoogleDiscoveryInvocationError({        message: `No Google Discovery source found for "${entry.namespace}"`,        statusCode: Option.none(),      });    }    const source = stored.config;     const authHeader =      source.auth.kind === "oauth2"        ? `Bearer ${yield* input.ctx.connections.accessToken(source.auth.connectionId).pipe(            Effect.mapError(              (err) =>                new GoogleDiscoveryOAuthError({                  message: errorMessageFromUnknown(err),                }),            ),          )}`        : undefined;     const layer = input.httpClientLayer ?? FetchHttpClient.layer;     return yield* performRequest({      method: entry.binding.method,      pathTemplate: entry.binding.pathTemplate,      parameters: entry.binding.parameters,      hasBody: entry.binding.hasBody,      source,      args: (input.args ?? {}) as Record<string, unknown>,      authorizationHeader: authHeader,    }).pipe(Effect.provide(layer)) as Effect.Effect<      GoogleDiscoveryInvocationResult,      GoogleDiscoveryInvocationError,      never    >;  });