Skip to content
Merged
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
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