368 lines · 11.3 KB · TypeScript
RawDownload
import {  applyCookiePlacementsToHeaders,  applyHttpQueryPlacementsToUrl,  applyJsonBodyPlacements,} from "@executor/codemode-core";import {  buildGraphqlToolPresentation,  compileGraphqlToolDefinitions,  extractGraphqlManifest,  GRAPHQL_INTROSPECTION_QUERY,  type GraphqlToolProviderData,} from "../graphql-tools";import type { Source } from "#schema";import { StringMapSchema } from "#schema";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema"; import {  createCatalogImportMetadata,  createGraphqlCatalogFragment,  type GraphqlCatalogOperationInput,} from "../source-catalog-snapshot";import { createSourceCatalogSyncResult } from "../source-catalog-support";import type { SourceAdapter } from "./types";import {  ConnectHttpAuthSchema,  ConnectHttpImportAuthSchema,  decodeBindingConfig,  decodeSourceBindingPayload,  emptySourceBindingState,  encodeBindingConfig,  isSourceCredentialRequiredError,  OptionalNullableStringSchema,  SourceCredentialRequiredError,  SourceConnectCommonFieldsSchema,} from "./shared"; const GraphqlConnectPayloadSchema = Schema.extend(  SourceConnectCommonFieldsSchema,  Schema.extend(    ConnectHttpImportAuthSchema,    Schema.Struct({      kind: Schema.Literal("graphql"),      auth: Schema.optional(ConnectHttpAuthSchema),    }),  ),); const GraphqlExecutorAddInputSchema = Schema.extend(  ConnectHttpImportAuthSchema,  Schema.Struct({    kind: Schema.Literal("graphql"),    endpoint: Schema.String,    name: OptionalNullableStringSchema,    namespace: OptionalNullableStringSchema,    auth: Schema.optional(ConnectHttpAuthSchema),  }),); const GraphqlBindingConfigSchema = Schema.Struct({  defaultHeaders: Schema.NullOr(StringMapSchema),}); type GraphqlBindingConfig = typeof GraphqlBindingConfigSchema.Type; const GraphqlSourceBindingPayloadSchema = Schema.Struct({  defaultHeaders: Schema.optional(Schema.NullOr(StringMapSchema)),}); const GRAPHQL_BINDING_CONFIG_VERSION = 1; const bindingHasAnyField = (  value: unknown,  fields: readonly string[],): boolean =>  value !== null  && typeof value === "object"  && !Array.isArray(value)  && fields.some((field) => Object.prototype.hasOwnProperty.call(value, field)); const graphqlBindingConfigFromSource = (  source: Pick<Source, "id" | "bindingVersion" | "binding">,): Effect.Effect<GraphqlBindingConfig, Error, never> =>  Effect.gen(function* () {    if (bindingHasAnyField(source.binding, ["transport", "queryParams", "headers"])) {      return yield* Effect.fail(        new Error("GraphQL sources cannot define MCP transport settings"),      );    }    if (bindingHasAnyField(source.binding, ["specUrl"])) {      return yield* Effect.fail(new Error("GraphQL sources cannot define specUrl"));    }     const bindingConfig = yield* decodeSourceBindingPayload({      sourceId: source.id,      label: "GraphQL",      version: source.bindingVersion,      expectedVersion: GRAPHQL_BINDING_CONFIG_VERSION,      schema: GraphqlSourceBindingPayloadSchema,      value: source.binding,      allowedKeys: ["defaultHeaders"],    });     return {      defaultHeaders: bindingConfig.defaultHeaders ?? null,    } satisfies GraphqlBindingConfig;  }); const fetchGraphqlIntrospectionDocumentWithHeaders = (input: {  url: string;  headers?: Readonly<Record<string, string>>;  queryParams?: Readonly<Record<string, string>>;  cookies?: Readonly<Record<string, string>>;  bodyValues?: Readonly<Record<string, string>>;}): Effect.Effect<string, Error, never> =>  Effect.tryPromise({    try: async () => {      const response = await fetch(        applyHttpQueryPlacementsToUrl({          url: input.url,          queryParams: input.queryParams,        }).toString(),        {        method: "POST",          headers: applyCookiePlacementsToHeaders({            headers: {              "content-type": "application/json",              ...(input.headers ?? {}),            },            cookies: input.cookies,          }),          body: JSON.stringify(            applyJsonBodyPlacements({              body: {                query: GRAPHQL_INTROSPECTION_QUERY,                operationName: "IntrospectionQuery",              },              bodyValues: input.bodyValues,              label: `GraphQL introspection ${input.url}`,            }),          ),        },      );      const text = await response.text();       let parsed: unknown;      try {        parsed = JSON.parse(text) as unknown;      } catch (cause) {        throw new Error(          `GraphQL introspection endpoint did not return JSON: ${cause instanceof Error ? cause.message : String(cause)}`,        );      }       if (!response.ok) {        if (response.status === 401 || response.status === 403) {          throw new SourceCredentialRequiredError(            "import",            `GraphQL introspection requires credentials (status ${response.status})`,          );        }        throw new Error(          `GraphQL introspection failed with status ${response.status}`,        );      }       return JSON.stringify(parsed, null, 2);    },    catch: (cause) =>      cause instanceof Error ? cause : new Error(String(cause)),  }); const graphqlCatalogOperationFromDefinition = (input: {  definition: ReturnType<typeof compileGraphqlToolDefinitions>[number];  manifest: Parameters<typeof buildGraphqlToolPresentation>[0]["manifest"];}): GraphqlCatalogOperationInput => {  const presentation = buildGraphqlToolPresentation({    manifest: input.manifest,    definition: input.definition,  });   return {    toolId: input.definition.toolId,    title: input.definition.name,    description: input.definition.description,    effect:      input.definition.operationType === "query"        ? "read"        : "write",    inputSchema: presentation.inputSchema,    outputSchema: presentation.outputSchema,    providerData: presentation.providerData as GraphqlToolProviderData,  };}; export const graphqlSourceAdapter: SourceAdapter = {  key: "graphql",  displayName: "GraphQL",  family: "http_api",  bindingConfigVersion: GRAPHQL_BINDING_CONFIG_VERSION,  providerKey: "generic_graphql",  defaultImportAuthPolicy: "reuse_runtime",  connectPayloadSchema: GraphqlConnectPayloadSchema,  executorAddInputSchema: GraphqlExecutorAddInputSchema,  executorAddHelpText: [    "endpoint is the GraphQL HTTP endpoint.",  ],  executorAddInputSignatureWidth: 320,  serializeBindingConfig: (source) =>    encodeBindingConfig({      adapterKey: "graphql",      version: GRAPHQL_BINDING_CONFIG_VERSION,      payloadSchema: GraphqlBindingConfigSchema,      payload: Effect.runSync(graphqlBindingConfigFromSource(source)),    }),  deserializeBindingConfig: ({ id, bindingConfigJson }) =>    Effect.map(      decodeBindingConfig({        sourceId: id,        label: "GraphQL",        adapterKey: "graphql",        version: GRAPHQL_BINDING_CONFIG_VERSION,        payloadSchema: GraphqlBindingConfigSchema,        value: bindingConfigJson,      }),      ({ version, payload }) => ({        version,        payload,      }),    ),  bindingStateFromSource: (source) =>    Effect.map(graphqlBindingConfigFromSource(source), (bindingConfig) => ({        ...emptySourceBindingState,        defaultHeaders: bindingConfig.defaultHeaders,      }),    ),  sourceConfigFromSource: (source) =>    Effect.runSync(      Effect.map(graphqlBindingConfigFromSource(source), (bindingConfig) => ({        kind: "graphql",        endpoint: source.endpoint,        defaultHeaders: bindingConfig.defaultHeaders,      })),    ),  validateSource: (source) =>    Effect.gen(function* () {      const bindingConfig = yield* graphqlBindingConfigFromSource(source);       return {        ...source,        bindingVersion: GRAPHQL_BINDING_CONFIG_VERSION,        binding: {          defaultHeaders: bindingConfig.defaultHeaders,        },      };    }),  shouldAutoProbe: (source) =>    source.enabled && (source.status === "draft" || source.status === "probing"),  syncCatalog: ({ source, resolveAuthMaterialForSlot }) =>    Effect.gen(function* () {      const bindingConfig = yield* graphqlBindingConfigFromSource(source);      const auth = yield* resolveAuthMaterialForSlot("import");      const graphqlDocument = yield* fetchGraphqlIntrospectionDocumentWithHeaders(        {          url: source.endpoint,          headers: {            ...(bindingConfig.defaultHeaders ?? {}),            ...auth.headers,          },          queryParams: auth.queryParams,          cookies: auth.cookies,          bodyValues: auth.bodyValues,        },      ).pipe(        Effect.withSpan("graphql.introspection.fetch", {          kind: "client",          attributes: {            "executor.source.id": source.id,            "executor.source.endpoint": source.endpoint,          },        }),        Effect.mapError(          (cause) =>            isSourceCredentialRequiredError(cause)              ? cause              : new Error(                `Failed fetching GraphQL introspection for ${source.id}: ${cause.message}`,              ),        ),      );       const manifest = yield* extractGraphqlManifest(        source.name,        graphqlDocument,      ).pipe(        Effect.withSpan("graphql.manifest.extract", {          attributes: {            "executor.source.id": source.id,          },        }),        Effect.mapError((cause) =>          cause instanceof Error ? cause : new Error(String(cause)),        ),      );      yield* Effect.annotateCurrentSpan("graphql.tool.count", manifest.tools.length);       const definitions = yield* Effect.sync(() => compileGraphqlToolDefinitions(manifest)).pipe(        Effect.withSpan("graphql.definitions.compile", {          attributes: {            "executor.source.id": source.id,            "graphql.tool.count": manifest.tools.length,          },        }),      );      yield* Effect.annotateCurrentSpan("graphql.definition.count", definitions.length);      const operations = yield* Effect.sync(() =>        definitions.map((definition) =>          graphqlCatalogOperationFromDefinition({            definition,            manifest,          })        )      ).pipe(        Effect.withSpan("graphql.operations.build", {          attributes: {            "executor.source.id": source.id,            "graphql.definition.count": definitions.length,          },        }),      );      const now = Date.now();      const fragment = yield* Effect.sync(() =>        createGraphqlCatalogFragment({          source,          documents: [{            documentKind: "graphql_introspection",            documentKey: source.endpoint,            contentText: graphqlDocument,            fetchedAt: now,          }],          operations,        })      ).pipe(        Effect.withSpan("graphql.snapshot.build", {          attributes: {            "executor.source.id": source.id,            "graphql.operation.count": operations.length,          },        }),      );       return createSourceCatalogSyncResult({        fragment,        importMetadata: createCatalogImportMetadata({          source,          adapterKey: "graphql",        }),        sourceHash: manifest.sourceHash,      });    }).pipe(      Effect.withSpan("graphql.syncCatalog", {        attributes: {          "executor.source.id": source.id,          "executor.source.endpoint": source.endpoint,        },      }),    ),};