Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 30 additions & 25 deletions packages/plugins/google-discovery/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,33 @@ const stringValuesFromParameter = (value: unknown, repeated: boolean): string[]
return [JSON.stringify(value)];
};

const replacePathParameters = (input: {
const replacePathParameters = Effect.fn("GoogleDiscovery.replacePathParameters")(function* (input: {
pathTemplate: string;
args: Record<string, unknown>;
parameters: readonly GoogleDiscoveryParameter[];
}): string =>
input.pathTemplate.replaceAll(/\{([^}]+)\}/g, (_, name: string) => {
}) {
let resolved = input.pathTemplate;
for (const match of input.pathTemplate.matchAll(/\{([^}]+)\}/g)) {
const placeholder = match[0];
const name = match[1]!;
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) {
throw new Error(`Missing required path parameter: ${name}`);
return yield* new GoogleDiscoveryInvocationError({
message: `Missing required path parameter: ${name}`,
statusCode: Option.none(),
});
}
return "";
resolved = resolved.replaceAll(placeholder, "");
} else {
resolved = resolved.replaceAll(placeholder, encodeURIComponent(values[0]!));
}
return encodeURIComponent(values[0]!);
});
}
return resolved;
});

const resolveBaseUrl = (source: GoogleDiscoveryStoredSourceData): string =>
new URL(source.servicePath || "", source.rootUrl).toString();
Expand Down Expand Up @@ -87,7 +96,7 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: {
}) {
const client = yield* HttpClient.HttpClient;

const resolvedPath = replacePathParameters({
const resolvedPath = yield* replacePathParameters({
pathTemplate: input.pathTemplate,
args: input.args,
parameters: input.parameters,
Expand Down Expand Up @@ -138,7 +147,7 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: {
Effect.mapError(
(err) =>
new GoogleDiscoveryInvocationError({
message: `HTTP request failed: ${err.message}`,
message: "HTTP request failed",
statusCode: Option.none(),
cause: err,
}),
Expand All @@ -147,9 +156,9 @@ const performRequest = Effect.fn("GoogleDiscovery.invoke")(function* (input: {

const contentType = response.headers["content-type"] ?? null;
const mapBodyError = Effect.mapError(
(err: { readonly message?: string }) =>
(err) =>
new GoogleDiscoveryInvocationError({
message: `Failed to read response body: ${err.message ?? String(err)}`,
message: "Failed to read response body",
statusCode: Option.some(response.status),
cause: err,
}),
Expand Down Expand Up @@ -191,31 +200,27 @@ export const invokeGoogleDiscoveryTool = (input: {
Effect.gen(function* () {
const entry = yield* input.ctx.storage.getBinding(input.toolId, input.toolScope);
if (!entry) {
return yield* Effect.fail(
new GoogleDiscoveryInvocationError({
message: `No Google Discovery operation found for tool "${input.toolId}"`,
statusCode: Option.none(),
}),
);
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* Effect.fail(
new GoogleDiscoveryInvocationError({
message: `No Google Discovery source found for "${entry.namespace}"`,
statusCode: Option.none(),
}),
);
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: "message" in err ? (err as { message: string }).message : String(err),
message: "Failed to resolve Google Discovery OAuth access token",
}),
),
)}`
Expand Down
175 changes: 113 additions & 62 deletions packages/plugins/graphql/src/sdk/introspect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Effect } from "effect";
import { Effect, Schema } from "effect";
import { HttpClient, HttpClientRequest } from "effect/unstable/http";

import { GraphqlIntrospectionError } from "./errors";
Expand Down Expand Up @@ -81,49 +81,99 @@ const INTROSPECTION_QUERY = `
// Introspection result types
// ---------------------------------------------------------------------------

export interface IntrospectionTypeRef {
readonly kind: string;
readonly name: string | null;
readonly ofType: IntrospectionTypeRef | null;
}

export interface IntrospectionInputValue {
readonly name: string;
readonly description: string | null;
readonly type: IntrospectionTypeRef;
readonly defaultValue: string | null;
}

export interface IntrospectionField {
readonly name: string;
readonly description: string | null;
readonly args: readonly IntrospectionInputValue[];
readonly type: IntrospectionTypeRef;
}

export interface IntrospectionEnumValue {
readonly name: string;
readonly description: string | null;
}

export interface IntrospectionType {
readonly kind: string;
readonly name: string;
readonly description: string | null;
readonly fields: readonly IntrospectionField[] | null;
readonly inputFields: readonly IntrospectionInputValue[] | null;
readonly enumValues: readonly IntrospectionEnumValue[] | null;
}

export interface IntrospectionSchema {
readonly queryType: { readonly name: string } | null;
readonly mutationType: { readonly name: string } | null;
readonly types: readonly IntrospectionType[];
}

export interface IntrospectionResult {
readonly __schema: IntrospectionSchema;
}
const IntrospectionTypeRefLeaf = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.Null,
});

const IntrospectionTypeRef5 = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.NullOr(IntrospectionTypeRefLeaf),
});

const IntrospectionTypeRef4 = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.NullOr(IntrospectionTypeRef5),
});

const IntrospectionTypeRef3 = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.NullOr(IntrospectionTypeRef4),
});

const IntrospectionTypeRef2 = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.NullOr(IntrospectionTypeRef3),
});

const IntrospectionTypeRefSchema = Schema.Struct({
kind: Schema.String,
name: Schema.NullOr(Schema.String),
ofType: Schema.NullOr(IntrospectionTypeRef2),
});

const IntrospectionInputValueSchema = Schema.Struct({
name: Schema.String,
description: Schema.NullOr(Schema.String),
type: IntrospectionTypeRefSchema,
defaultValue: Schema.NullOr(Schema.String),
});

const IntrospectionFieldSchema = Schema.Struct({
name: Schema.String,
description: Schema.NullOr(Schema.String),
args: Schema.Array(IntrospectionInputValueSchema),
type: IntrospectionTypeRefSchema,
});

const IntrospectionTypeSchema = Schema.Struct({
kind: Schema.String,
name: Schema.String,
description: Schema.NullOr(Schema.String),
fields: Schema.NullOr(Schema.Array(IntrospectionFieldSchema)),
inputFields: Schema.NullOr(Schema.Array(IntrospectionInputValueSchema)),
enumValues: Schema.NullOr(
Schema.Array(
Schema.Struct({
name: Schema.String,
description: Schema.NullOr(Schema.String),
}),
),
),
});

const IntrospectionResultSchema = Schema.Struct({
__schema: Schema.Struct({
queryType: Schema.NullOr(Schema.Struct({ name: Schema.String })),
mutationType: Schema.NullOr(Schema.Struct({ name: Schema.String })),
types: Schema.Array(IntrospectionTypeSchema),
}),
});

const IntrospectionResponseSchema = Schema.Struct({
data: Schema.optional(IntrospectionResultSchema),
errors: Schema.optional(Schema.Array(Schema.Unknown)),
});

const IntrospectionJsonSchema = Schema.Union([
Schema.Struct({ data: IntrospectionResultSchema }),
IntrospectionResultSchema,
]);

export type IntrospectionTypeRef = typeof IntrospectionTypeRefSchema.Type;
export type IntrospectionInputValue = typeof IntrospectionInputValueSchema.Type;
export type IntrospectionField = typeof IntrospectionFieldSchema.Type;
export type IntrospectionEnumValue = NonNullable<
(typeof IntrospectionTypeSchema.Type)["enumValues"]
>[number];
export type IntrospectionType = typeof IntrospectionTypeSchema.Type;
export type IntrospectionSchema = (typeof IntrospectionResultSchema.Type)["__schema"];
export type IntrospectionResult = typeof IntrospectionResultSchema.Type;

// ---------------------------------------------------------------------------
// Introspect a GraphQL endpoint
Expand Down Expand Up @@ -162,9 +212,9 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* (
const response = yield* client.execute(request).pipe(
Effect.tapCause((cause) => Effect.logError("graphql introspection request failed", cause)),
Effect.mapError(
(err) =>
() =>
new GraphqlIntrospectionError({
message: `Failed to reach GraphQL endpoint: ${err.message}`,
message: "Failed to reach GraphQL endpoint",
}),
),
);
Expand All @@ -187,7 +237,14 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* (
),
);

const json = raw as { data?: IntrospectionResult; errors?: unknown[] };
const json = yield* Schema.decodeUnknownEffect(IntrospectionResponseSchema)(raw).pipe(
Effect.mapError(
() =>
new GraphqlIntrospectionError({
message: "Introspection response has an invalid shape",
}),
),
);

if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {
return yield* new GraphqlIntrospectionError({
Expand All @@ -211,18 +268,12 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* (
export const parseIntrospectionJson = (
text: string,
): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> =>
Effect.try({
try: () => {
const parsed = JSON.parse(text);
// Accept both { data: { __schema } } and { __schema } formats
const result = parsed.data ?? parsed;
if (!result.__schema) {
throw new Error("Missing __schema in introspection JSON");
}
return result as IntrospectionResult;
},
catch: (err) =>
new GraphqlIntrospectionError({
message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`,
}),
});
Schema.decodeUnknownEffect(Schema.fromJsonString(IntrospectionJsonSchema))(text).pipe(
Effect.map((parsed) => ("data" in parsed ? parsed.data : parsed)),
Effect.mapError(
() =>
new GraphqlIntrospectionError({
message: "Failed to parse introspection JSON",
}),
),
);
11 changes: 1 addition & 10 deletions packages/plugins/graphql/src/sdk/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* (
Effect.mapError(
(err) =>
new GraphqlInvocationError({
message: `GraphQL request failed: ${err.message}`,
message: "GraphQL request failed",
statusCode: Option.none(),
cause: err,
}),
Expand Down Expand Up @@ -159,15 +159,6 @@ export const invokeWithLayer = (
) =>
invoke(operation, args, endpoint, resolvedHeaders, resolvedQueryParams).pipe(
Effect.provide(httpClientLayer),
Effect.mapError((err) =>
err instanceof GraphqlInvocationError
? err
: new GraphqlInvocationError({
message: err instanceof Error ? err.message : String(err),
statusCode: Option.none(),
cause: err,
}),
),
Effect.withSpan("plugin.graphql.invoke", {
attributes: {
"plugin.graphql.endpoint": endpoint,
Expand Down
Loading