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. Load more
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",