730 lines · 27.0 KB · TypeScript
RawDownload
import { Effect, Match, Option, Schema } from "effect";import type { Layer } from "effect";import { HttpClient } from "effect/unstable/http"; import {  authToolFailure,  definePlugin,  IntegrationAlreadyExistsError,  IntegrationSlug,  ToolName,  ToolResult,  tool,  type IntegrationConfig,  type PluginCtx,  type ToolAnnotations,  type ToolDef,  type ToolInvocationCredential,} from "@executor-js/sdk"; import {  introspect,  parseIntrospectionJson,  type IntrospectionResult,  type IntrospectionType,  type IntrospectionField,  type IntrospectionTypeRef,} from "./introspect";import { extract } from "./extract";import {  GraphqlAuthRequiredError,  GraphqlExtractionError,  GraphqlIntrospectionError,  GraphqlInvocationError,} from "./errors";import { invokeWithLayer } from "./invoke";import { graphqlPresets } from "./presets";import {  type ApiKeyHeaderTemplate,  type ApiKeyQueryTemplate,  AuthTemplate,  ExtractedField,  GraphqlIntegrationConfig,  type GraphqlOperationKind,  type OAuthTemplate,  OperationBinding,} from "./types"; // ---------------------------------------------------------------------------// Plugin config// --------------------------------------------------------------------------- const GraphqlErrorBody = Schema.Struct({ message: Schema.String });const GraphqlErrorsBody = Schema.Array(Schema.Unknown);const decodeGraphqlErrorBody = Schema.decodeUnknownOption(GraphqlErrorBody);const decodeGraphqlErrorsBody = Schema.decodeUnknownOption(GraphqlErrorsBody);const decodeIntegrationConfig = Schema.decodeUnknownOption(GraphqlIntegrationConfig); const decodeGraphqlErrors = (errors: unknown): readonly unknown[] | undefined =>  Option.getOrUndefined(decodeGraphqlErrorsBody(errors)); const extractGraphqlErrorMessage = (errors: readonly unknown[]): string | undefined =>  errors    .map((error: unknown) => Option.getOrUndefined(decodeGraphqlErrorBody(error))?.message)    .find((message: string | undefined) => message !== undefined && message.length > 0); const GRAPHQL_PLUGIN_ID = "graphql-greenfield"; /** The default auth template a connection applies through when the integration *  declares no explicit `authentication`. Renders the value as a bearer header. */const DEFAULT_OAUTH_TEMPLATE_SLUG = "oauth"; // ---------------------------------------------------------------------------// Public input contracts — derived from the Effect Schemas so the wire shape// and the TypeScript type stay in lockstep.// --------------------------------------------------------------------------- const AddGraphqlIntegrationInputSchema = Schema.Struct({  /** The integration slug (catalog identity). */  slug: Schema.String,  /** The GraphQL endpoint URL. */  endpoint: Schema.String,  /** Display description for the catalog. */  description: Schema.optional(Schema.String),  /** Optional introspection JSON (when the endpoint can't be reached live). */  introspectionJson: Schema.optional(Schema.String),  /** Auth methods a connection can apply through. Defaults to a single bearer   *  oauth template if omitted. */  authentication: Schema.optional(Schema.Array(AuthTemplate)),  /** Headers applied to the add-time introspection request only. */  introspectionHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)),});export type AddGraphqlIntegrationInput = typeof AddGraphqlIntegrationInputSchema.Type; const ConfigureGraphqlIntegrationInputSchema = Schema.Struct({  slug: Schema.String,  endpoint: Schema.optional(Schema.String),  description: Schema.optional(Schema.String),  authentication: Schema.optional(Schema.Array(AuthTemplate)),});/** Patch applied by `configureIntegration` (the `slug` identifies which one). */export type ConfigureGraphqlIntegrationInput = Omit<  typeof ConfigureGraphqlIntegrationInputSchema.Type,  "slug">; const ConfigureGraphqlIntegrationOutputSchema = Schema.Struct({  configured: Schema.Boolean,}); const StaticAddIntegrationOutputSchema = Schema.Struct({  slug: Schema.String,  toolCount: Schema.Number,});const StaticGetIntegrationInputSchema = Schema.Struct({ slug: Schema.String });const StaticGetIntegrationOutputSchema = Schema.Struct({  integration: Schema.NullOr(Schema.Unknown),}); const toStandard = <A, I>(schema: Schema.Codec<A, I>) =>  Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(schema)); const StaticAddIntegrationInputStandardSchema = toStandard(AddGraphqlIntegrationInputSchema);const StaticAddIntegrationOutputStandardSchema = toStandard(StaticAddIntegrationOutputSchema);const StaticGetIntegrationInputStandardSchema = toStandard(StaticGetIntegrationInputSchema);const StaticGetIntegrationOutputStandardSchema = toStandard(StaticGetIntegrationOutputSchema);const StaticConfigureIntegrationInputStandardSchema = toStandard(  ConfigureGraphqlIntegrationInputSchema,);const StaticConfigureIntegrationOutputStandardSchema = toStandard(  ConfigureGraphqlIntegrationOutputSchema,); const graphqlToolFailure = (code: string, message: string, details?: unknown) =>  ToolResult.fail({    code,    message,    ...(details === undefined ? {} : { details }),  }); const graphqlAuthToolFailure = (failure: GraphqlAuthRequiredError) =>  authToolFailure({    code: failure.code,    message: failure.message,    source: { id: failure.integration },    credential: {      kind: failure.credentialKind,      ...(failure.credentialLabel ? { label: failure.credentialLabel } : {}),    },    ...(failure.status !== undefined ? { status: failure.status } : {}),    ...(failure.details !== undefined      ? {          upstream: {            ...(failure.status !== undefined ? { status: failure.status } : {}),            details: failure.details,          },        }      : {}),    recovery: { configureSourceTool: "executor.graphql-greenfield.configureIntegration" },  }); // ---------------------------------------------------------------------------// Detection helpers// --------------------------------------------------------------------------- /** Match `token` as a separator-bounded run inside a URL hostname or path, *  used as a low-confidence detection hint when introspection fails. */const urlMatchesToken = (url: URL, token: string): boolean => {  const re = new RegExp(`(?:^|[^a-z0-9])${token}(?:$|[^a-z0-9])`, "i");  return re.test(url.hostname) || re.test(url.pathname);}; /** Derive a slug from an endpoint URL. */const slugFromEndpoint = (endpoint: string): string => {  // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: URL construction throws; this helper intentionally falls back to the stable default slug  try {    const url = new URL(endpoint);    return url.hostname.replace(/[^a-z0-9]+/gi, "_").toLowerCase();  } catch {    return "graphql";  }}; // ---------------------------------------------------------------------------// Operation-string building// --------------------------------------------------------------------------- const formatTypeRef = (ref: IntrospectionTypeRef): string =>  Match.value(ref.kind).pipe(    Match.when("NON_NULL", () => (ref.ofType ? `${formatTypeRef(ref.ofType)}!` : "Unknown!")),    Match.when("LIST", () => (ref.ofType ? `[${formatTypeRef(ref.ofType)}]` : "[Unknown]")),    Match.option,    Option.getOrElse(() => ref.name ?? "Unknown"),  ); const unwrapTypeName = (ref: IntrospectionTypeRef): string => {  if (ref.name) return ref.name;  if (ref.ofType) return unwrapTypeName(ref.ofType);  return "Unknown";}; const buildSelectionSet = (  ref: IntrospectionTypeRef,  types: ReadonlyMap<string, IntrospectionType>,  depth: number,  seen: Set<string>,): string => {  if (depth > 2) return "";   const leafName = unwrapTypeName(ref);  if (seen.has(leafName)) return "";   const objectType = types.get(leafName);  if (!objectType?.fields) return "";   const kind = objectType.kind;  if (kind === "SCALAR" || kind === "ENUM") return "";   seen.add(leafName);   const subFields = objectType.fields    .filter((f: IntrospectionField) => !f.name.startsWith("__"))    .slice(0, 12)    .map((f: IntrospectionField) => {      const sub = buildSelectionSet(f.type, types, depth + 1, seen);      return sub ? `${f.name} ${sub}` : f.name;    });   seen.delete(leafName);   return subFields.length > 0 ? `{ ${subFields.join(" ")} }` : "";}; const buildOperationStringForField = (  kind: GraphqlOperationKind,  field: IntrospectionField,  types: ReadonlyMap<string, IntrospectionType>,): string => {  const opType = kind === "query" ? "query" : "mutation";   const varDefs = field.args.map((arg) => {    const typeName = formatTypeRef(arg.type);    return `$${arg.name}: ${typeName}`;  });   const argPasses = field.args.map((arg) => `${arg.name}: $${arg.name}`);  const selectionSet = buildSelectionSet(field.type, types, 0, new Set());   const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(", ")})` : "";  const argPassStr = argPasses.length > 0 ? `(${argPasses.join(", ")})` : "";   return `${opType}${varDefsStr} { ${field.name}${argPassStr}${selectionSet ? ` ${selectionSet}` : ""} }`;}; const prepareOperations = (  fields: readonly ExtractedField[],  introspection: IntrospectionResult,): readonly OperationBinding[] => {  const typeMap = new Map<string, IntrospectionType>();  for (const t of introspection.__schema.types) {    typeMap.set(t.name, t);  }   const fieldMap = new Map<string, { kind: GraphqlOperationKind; field: IntrospectionField }>();  const schema = introspection.__schema;  for (const rootKind of ["query", "mutation"] as const) {    const typeName = rootKind === "query" ? schema.queryType?.name : schema.mutationType?.name;    if (!typeName) continue;    const rootType = typeMap.get(typeName);    if (!rootType?.fields) continue;    for (const f of rootType.fields) {      if (!f.name.startsWith("__")) {        fieldMap.set(`${rootKind}.${f.name}`, { kind: rootKind, field: f });      }    }  }   return fields.map((extracted: ExtractedField) => {    const prefix = extracted.kind === "mutation" ? "mutation" : "query";    // A tool's name keeps its `<kind>.<field>` path (e.g. `query.hello`,    // `mutation.setGreeting`). The address grammar treats `<tool>` as the    // trailing remainder (see parseToolAddress), so the dot nests naturally.    const toolName = `${prefix}.${extracted.fieldName}`;    const description = Option.getOrElse(      extracted.description,      () => `GraphQL ${extracted.kind}: ${extracted.fieldName} -> ${extracted.returnTypeName}`,    );     const key = `${extracted.kind}.${extracted.fieldName}`;    const entry = fieldMap.get(key);    const operationString = entry      ? buildOperationStringForField(entry.kind, entry.field, typeMap)      : `${extracted.kind} { ${extracted.fieldName} }`;     return OperationBinding.make({      toolName,      kind: extracted.kind,      fieldName: extracted.fieldName,      operationString,      variableNames: extracted.arguments.map((a) => a.name),      description,      inputSchema: Option.getOrUndefined(extracted.inputSchema),    });  });}; const annotationsFor = (binding: OperationBinding): ToolAnnotations | undefined => {  if (binding.kind === "mutation") {    return {      requiresApproval: true,      approvalDescription: `mutation ${binding.fieldName}`,    };  }  return undefined;}; // ---------------------------------------------------------------------------// Auth-template rendering — D11: the plugin derives the resolved connection// value into the integration's auth-template format. An OAuth access token and// an apiKey both arrive as a single `value` and render identically.// --------------------------------------------------------------------------- interface RenderedAuth {  readonly headers: Record<string, string>;  readonly queryParams: Record<string, string>;} const selectAuthTemplate = (  authentication: readonly AuthTemplate[],  templateSlug: string,): AuthTemplate | null => {  const match = authentication.find((t: AuthTemplate) => t.slug === templateSlug);  if (match) return match;  // Fall back to the first declared template, or a synthesized default oauth.  if (authentication.length > 0) return authentication[0] ?? null;  return null;}; const renderAuth = (template: AuthTemplate | null, value: string): RenderedAuth => {  const headers: Record<string, string> = {};  const queryParams: Record<string, string> = {};   if (template === null) {    // No declared template — default to a bearer Authorization header.    headers.Authorization = `Bearer ${value}`;    return { headers, queryParams };  }   Match.value(template).pipe(    Match.when({ type: "apiKey", in: "header" }, (t: ApiKeyHeaderTemplate) => {      headers[t.name] = `${t.prefix ?? ""}${value}`;    }),    Match.when({ type: "apiKey", in: "query" }, (t: ApiKeyQueryTemplate) => {      queryParams[t.name] = `${t.prefix ?? ""}${value}`;    }),    Match.when({ type: "oauth" }, (t: OAuthTemplate) => {      const header = t.header ?? "Authorization";      const prefix = t.prefix ?? "Bearer ";      headers[header] = `${prefix}${value}`;    }),    Match.exhaustive,  );   return { headers, queryParams };}; // ---------------------------------------------------------------------------// Plugin factory// --------------------------------------------------------------------------- export interface GraphqlPluginOptions {  readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>;} const defaultAuthentication = (  authentication: readonly AuthTemplate[] | undefined,): readonly AuthTemplate[] =>  authentication && authentication.length > 0    ? authentication    : [AuthTemplate.make({ slug: DEFAULT_OAUTH_TEMPLATE_SLUG, type: "oauth" })]; const integrationConfigFrom = (config: IntegrationConfig): GraphqlIntegrationConfig | null =>  Option.getOrNull(decodeIntegrationConfig(config)); const makeGraphqlExtension = (  ctx: PluginCtx<Record<string, never>>,  httpClientLayer: Layer.Layer<HttpClient.HttpClient>,) => {  const buildConfig = (input: AddGraphqlIntegrationInput) =>    Effect.gen(function* () {      let introspectionResult: IntrospectionResult;      if (input.introspectionJson) {        introspectionResult = yield* parseIntrospectionJson(input.introspectionJson);      } else {        introspectionResult = yield* introspect(input.endpoint, input.introspectionHeaders).pipe(          Effect.provide(httpClientLayer),        );      }       const { result, definitions } = yield* extract(introspectionResult);      const operations = prepareOperations(result.fields, introspectionResult);       const config = GraphqlIntegrationConfig.make({        endpoint: input.endpoint,        ...(input.introspectionJson ? { introspectionJson: input.introspectionJson } : {}),        authentication: defaultAuthentication(input.authentication),        operations,        ...(Object.keys(definitions).length > 0 ? { definitions } : {}),      });      return { config, toolCount: operations.length };    });   const addIntegration = (input: AddGraphqlIntegrationInput) =>    Effect.gen(function* () {      const slug = IntegrationSlug.make(input.slug);       // Block re-adding an existing slug. The core `integrations.register`      // primitive upserts (so boot re-registration is idempotent), but an      // explicit add must NOT silently clobber an existing integration's tools,      // connections, and policies. To add more auth, update the existing one.      const existing = yield* ctx.core.integrations.get(slug);      if (existing) {        return yield* new IntegrationAlreadyExistsError({ slug });      }       return yield* ctx.transaction(        Effect.gen(function* () {          const { config, toolCount } = yield* buildConfig(input);          yield* ctx.core.integrations.register({            slug,            description: input.description ?? input.slug,            config,            canRemove: true,            canRefresh: true,          });          return { slug: input.slug, toolCount };        }),      );    });   const getIntegration = (slug: string) =>    ctx.core.integrations.get(IntegrationSlug.make(slug)).pipe(      Effect.map((record) => {        if (record === null) return null;        const config = integrationConfigFrom(record.config);        return { ...record, config };      }),    );   const configureIntegration = (slug: string, input: ConfigureGraphqlIntegrationInput) =>    Effect.gen(function* () {      const integrationSlug = IntegrationSlug.make(slug);      const record = yield* ctx.core.integrations.get(integrationSlug);      if (record === null) return;      const existing = integrationConfigFrom(record.config);      if (existing === null) return;      const nextConfig = GraphqlIntegrationConfig.make({        ...existing,        endpoint: input.endpoint ?? existing.endpoint,        authentication: input.authentication ?? existing.authentication,      });      yield* ctx.core.integrations.update(integrationSlug, {        ...(input.description ? { description: input.description } : {}),        config: nextConfig,      });    });   const removeIntegration = (slug: string) =>    ctx.core.integrations.remove(IntegrationSlug.make(slug));   return {    addIntegration,    getIntegration,    configureIntegration,    removeIntegration,  };}; export type GraphqlPluginExtension = ReturnType<typeof makeGraphqlExtension>; export const graphqlPlugin = definePlugin((options?: GraphqlPluginOptions) => {  return {    id: GRAPHQL_PLUGIN_ID as typeof GRAPHQL_PLUGIN_ID,    packageName: "@executor-js/plugin-graphql-greenfield",    integrationPresets: graphqlPresets.map((preset) => ({      id: preset.id,      name: preset.name,      summary: preset.summary,      url: preset.url,      endpoint: preset.endpoint,      ...(preset.icon ? { icon: preset.icon } : {}),      ...(preset.featured ? { featured: preset.featured } : {}),    })),    storage: (): Record<string, never> => ({}),     extension: (ctx) => makeGraphqlExtension(ctx, options?.httpClientLayer ?? ctx.httpClientLayer),     staticSources: (self) => [      {        id: GRAPHQL_PLUGIN_ID,        kind: "executor",

Lines 1–500 of 730.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t