History

Use typed GraphQL invocation boundaries

Rhys Sullivan committed 6 months ago

Commit 83b84eb1 parent b039b6e2 files · +114 −72Browse files

Unified · Split

2 files
  1. packages/plugins/graphql/src/sdk/introspect.ts +113 −62
  2. packages/plugins/graphql/src/sdk/invoke.ts +1 −10
packages/plugins/graphql/src/sdk/introspect.ts+113−62
@@ -1,4 +1,4 @@
1−import { Effect } from "effect";
1+import { Effect, Schema } from "effect";
22 import { HttpClient, HttpClientRequest } from "effect/unstable/http";
33
44 import { GraphqlIntrospectionError } from "./errors";
@@ -81,49 +81,99 @@ const INTROSPECTION_QUERY = ` · 76 unmodified lines
8181 // Introspection result types
8282 // ---------------------------------------------------------------------------
8383
84−export interface IntrospectionTypeRef {
85− readonly kind: string;
86− readonly name: string | null;
87− readonly ofType: IntrospectionTypeRef | null;
88−}
89−
90−export interface IntrospectionInputValue {
91− readonly name: string;
92− readonly description: string | null;
93− readonly type: IntrospectionTypeRef;
94− readonly defaultValue: string | null;
95−}
96−
97−export interface IntrospectionField {
98− readonly name: string;
99− readonly description: string | null;
100− readonly args: readonly IntrospectionInputValue[];
101− readonly type: IntrospectionTypeRef;
102−}
103−
104−export interface IntrospectionEnumValue {
105− readonly name: string;
106− readonly description: string | null;
107−}
108−
109−export interface IntrospectionType {
110− readonly kind: string;
111− readonly name: string;
112− readonly description: string | null;
113− readonly fields: readonly IntrospectionField[] | null;
114− readonly inputFields: readonly IntrospectionInputValue[] | null;
115− readonly enumValues: readonly IntrospectionEnumValue[] | null;
116−}
117−
118−export interface IntrospectionSchema {
119− readonly queryType: { readonly name: string } | null;
120− readonly mutationType: { readonly name: string } | null;
121− readonly types: readonly IntrospectionType[];
122−}
123−
124−export interface IntrospectionResult {
125− readonly __schema: IntrospectionSchema;
126−}
84+const IntrospectionTypeRefLeaf = Schema.Struct({
85+ kind: Schema.String,
86+ name: Schema.NullOr(Schema.String),
87+ ofType: Schema.Null,
88+});
89+
90+const IntrospectionTypeRef5 = Schema.Struct({
91+ kind: Schema.String,
92+ name: Schema.NullOr(Schema.String),
93+ ofType: Schema.NullOr(IntrospectionTypeRefLeaf),
94+});
95+
96+const IntrospectionTypeRef4 = Schema.Struct({
97+ kind: Schema.String,
98+ name: Schema.NullOr(Schema.String),
99+ ofType: Schema.NullOr(IntrospectionTypeRef5),
100+});
101+
102+const IntrospectionTypeRef3 = Schema.Struct({
103+ kind: Schema.String,
104+ name: Schema.NullOr(Schema.String),
105+ ofType: Schema.NullOr(IntrospectionTypeRef4),
106+});
107+
108+const IntrospectionTypeRef2 = Schema.Struct({
109+ kind: Schema.String,
110+ name: Schema.NullOr(Schema.String),
111+ ofType: Schema.NullOr(IntrospectionTypeRef3),
112+});
113+
114+const IntrospectionTypeRefSchema = Schema.Struct({
115+ kind: Schema.String,
116+ name: Schema.NullOr(Schema.String),
117+ ofType: Schema.NullOr(IntrospectionTypeRef2),
118+});
119+
120+const IntrospectionInputValueSchema = Schema.Struct({
121+ name: Schema.String,
122+ description: Schema.NullOr(Schema.String),
123+ type: IntrospectionTypeRefSchema,
124+ defaultValue: Schema.NullOr(Schema.String),
125+});
126+
127+const IntrospectionFieldSchema = Schema.Struct({
128+ name: Schema.String,
129+ description: Schema.NullOr(Schema.String),
130+ args: Schema.Array(IntrospectionInputValueSchema),
131+ type: IntrospectionTypeRefSchema,
132+});
133+
134+const IntrospectionTypeSchema = Schema.Struct({
135+ kind: Schema.String,
136+ name: Schema.String,
137+ description: Schema.NullOr(Schema.String),
138+ fields: Schema.NullOr(Schema.Array(IntrospectionFieldSchema)),
139+ inputFields: Schema.NullOr(Schema.Array(IntrospectionInputValueSchema)),
140+ enumValues: Schema.NullOr(
141+ Schema.Array(
142+ Schema.Struct({
143+ name: Schema.String,
144+ description: Schema.NullOr(Schema.String),
145+ }),
146+ ),
147+ ),
148+});
149+
150+const IntrospectionResultSchema = Schema.Struct({
151+ __schema: Schema.Struct({
152+ queryType: Schema.NullOr(Schema.Struct({ name: Schema.String })),
153+ mutationType: Schema.NullOr(Schema.Struct({ name: Schema.String })),
154+ types: Schema.Array(IntrospectionTypeSchema),
155+ }),
156+});
157+
158+const IntrospectionResponseSchema = Schema.Struct({
159+ data: Schema.optional(IntrospectionResultSchema),
160+ errors: Schema.optional(Schema.Array(Schema.Unknown)),
161+});
162+
163+const IntrospectionJsonSchema = Schema.Union([
164+ Schema.Struct({ data: IntrospectionResultSchema }),
165+ IntrospectionResultSchema,
166+]);
167+
168+export type IntrospectionTypeRef = typeof IntrospectionTypeRefSchema.Type;
169+export type IntrospectionInputValue = typeof IntrospectionInputValueSchema.Type;
170+export type IntrospectionField = typeof IntrospectionFieldSchema.Type;
171+export type IntrospectionEnumValue = NonNullable<
172+ (typeof IntrospectionTypeSchema.Type)["enumValues"]
173+>[number];
174+export type IntrospectionType = typeof IntrospectionTypeSchema.Type;
175+export type IntrospectionSchema = (typeof IntrospectionResultSchema.Type)["__schema"];
176+export type IntrospectionResult = typeof IntrospectionResultSchema.Type;
127177
128178 // ---------------------------------------------------------------------------
129179 // Introspect a GraphQL endpoint
@@ -162,9 +212,9 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 32 unmodified lines
162212 const response = yield* client.execute(request).pipe(
163213 Effect.tapCause((cause) => Effect.logError("graphql introspection request failed", cause)),
164214 Effect.mapError(
165− (err) =>
215+ () =>
166216 new GraphqlIntrospectionError({
167− message: `Failed to reach GraphQL endpoint: ${err.message}`,
217+ message: "Failed to reach GraphQL endpoint",
168218 }),
169219 ),
170220 );
@@ -187,7 +237,14 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 16 unmodified lines
187237 ),
188238 );
189239
190− const json = raw as { data?: IntrospectionResult; errors?: unknown[] };
240+ const json = yield* Schema.decodeUnknownEffect(IntrospectionResponseSchema)(raw).pipe(
241+ Effect.mapError(
242+ () =>
243+ new GraphqlIntrospectionError({
244+ message: "Introspection response has an invalid shape",
245+ }),
246+ ),
247+ );
191248
192249 if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {
193250 return yield* new GraphqlIntrospectionError({
@@ -211,18 +268,12 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 17 unmodified lines
211268 export const parseIntrospectionJson = (
212269 text: string,
213270 ): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> =>
214− Effect.try({
215− try: () => {
216− const parsed = JSON.parse(text);
217− // Accept both { data: { __schema } } and { __schema } formats
218− const result = parsed.data ?? parsed;
219− if (!result.__schema) {
220− throw new Error("Missing __schema in introspection JSON");
221− }
222− return result as IntrospectionResult;
223− },
224− catch: (err) =>
225− new GraphqlIntrospectionError({
226− message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`,
227− }),
228− });
271+ Schema.decodeUnknownEffect(Schema.fromJsonString(IntrospectionJsonSchema))(text).pipe(
272+ Effect.map((parsed) => ("data" in parsed ? parsed.data : parsed)),
273+ Effect.mapError(
274+ () =>
275+ new GraphqlIntrospectionError({
276+ message: "Failed to parse introspection JSON",
277+ }),
278+ ),
279+ );
packages/plugins/graphql/src/sdk/invoke.ts+1−10
@@ -114,7 +114,7 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 113 unmodified lines
114114 Effect.mapError(
115115 (err) =>
116116 new GraphqlInvocationError({
117− message: `GraphQL request failed: ${err.message}`,
117+ message: "GraphQL request failed",
118118 statusCode: Option.none(),
119119 cause: err,
120120 }),
@@ -159,15 +159,6 @@ export const invokeWithLayer = ( · 38 unmodified lines
159159 ) =>
160160 invoke(operation, args, endpoint, resolvedHeaders, resolvedQueryParams).pipe(
161161 Effect.provide(httpClientLayer),
162− Effect.mapError((err) =>
163− err instanceof GraphqlInvocationError
164− ? err
165− : new GraphqlInvocationError({
166− message: err instanceof Error ? err.message : String(err),
167− statusCode: Option.none(),
168− cause: err,
169− }),
170− ),
171162 Effect.withSpan("plugin.graphql.invoke", {
172163 attributes: {
173164 "plugin.graphql.endpoint": endpoint,