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 @@
1import { Effect } from "effect";1import { Effect, Schema } from "effect";
2import { HttpClient, HttpClientRequest } from "effect/unstable/http";2import { HttpClient, HttpClientRequest } from "effect/unstable/http";
3 3
4import { GraphqlIntrospectionError } from "./errors";4import { GraphqlIntrospectionError } from "./errors";
@@ -81,49 +81,99 @@ const INTROSPECTION_QUERY = ` · 76 unmodified lines
81// Introspection result types81// Introspection result types
82// ---------------------------------------------------------------------------82// ---------------------------------------------------------------------------
83 83
84export interface IntrospectionTypeRef {84const IntrospectionTypeRefLeaf = Schema.Struct({
85 readonly kind: string;85 kind: Schema.String,
86 readonly name: string | null;86 name: Schema.NullOr(Schema.String),
87 readonly ofType: IntrospectionTypeRef | null;87 ofType: Schema.Null,
88}88});
89 89
90export interface IntrospectionInputValue {90const IntrospectionTypeRef5 = Schema.Struct({
91 readonly name: string;91 kind: Schema.String,
92 readonly description: string | null;92 name: Schema.NullOr(Schema.String),
93 readonly type: IntrospectionTypeRef;93 ofType: Schema.NullOr(IntrospectionTypeRefLeaf),
94 readonly defaultValue: string | null;94});
95}95
96 96const IntrospectionTypeRef4 = Schema.Struct({
97export interface IntrospectionField {97 kind: Schema.String,
98 readonly name: string;98 name: Schema.NullOr(Schema.String),
99 readonly description: string | null;99 ofType: Schema.NullOr(IntrospectionTypeRef5),
100 readonly args: readonly IntrospectionInputValue[];100});
101 readonly type: IntrospectionTypeRef;101
102}102const IntrospectionTypeRef3 = Schema.Struct({
103 103 kind: Schema.String,
104export interface IntrospectionEnumValue {104 name: Schema.NullOr(Schema.String),
105 readonly name: string;105 ofType: Schema.NullOr(IntrospectionTypeRef4),
106 readonly description: string | null;106});
107}107
108 108const IntrospectionTypeRef2 = Schema.Struct({
109export interface IntrospectionType {109 kind: Schema.String,
110 readonly kind: string;110 name: Schema.NullOr(Schema.String),
111 readonly name: string;111 ofType: Schema.NullOr(IntrospectionTypeRef3),
112 readonly description: string | null;112});
113 readonly fields: readonly IntrospectionField[] | null;113
114 readonly inputFields: readonly IntrospectionInputValue[] | null;114const IntrospectionTypeRefSchema = Schema.Struct({
115 readonly enumValues: readonly IntrospectionEnumValue[] | null;115 kind: Schema.String,
116}116 name: Schema.NullOr(Schema.String),
117 117 ofType: Schema.NullOr(IntrospectionTypeRef2),
118export interface IntrospectionSchema {118});
119 readonly queryType: { readonly name: string } | null;119
120 readonly mutationType: { readonly name: string } | null;120const IntrospectionInputValueSchema = Schema.Struct({
121 readonly types: readonly IntrospectionType[];121 name: Schema.String,
122}122 description: Schema.NullOr(Schema.String),
123 123 type: IntrospectionTypeRefSchema,
124export interface IntrospectionResult {124 defaultValue: Schema.NullOr(Schema.String),
125 readonly __schema: IntrospectionSchema;125});
126}126
127const IntrospectionFieldSchema = Schema.Struct({
128 name: Schema.String,
129 description: Schema.NullOr(Schema.String),
130 args: Schema.Array(IntrospectionInputValueSchema),
131 type: IntrospectionTypeRefSchema,
132});
133
134const 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
150const 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
158const IntrospectionResponseSchema = Schema.Struct({
159 data: Schema.optional(IntrospectionResultSchema),
160 errors: Schema.optional(Schema.Array(Schema.Unknown)),
161});
162
163const IntrospectionJsonSchema = Schema.Union([
164 Schema.Struct({ data: IntrospectionResultSchema }),
165 IntrospectionResultSchema,
166]);
167
168export type IntrospectionTypeRef = typeof IntrospectionTypeRefSchema.Type;
169export type IntrospectionInputValue = typeof IntrospectionInputValueSchema.Type;
170export type IntrospectionField = typeof IntrospectionFieldSchema.Type;
171export type IntrospectionEnumValue = NonNullable<
172 (typeof IntrospectionTypeSchema.Type)["enumValues"]
173>[number];
174export type IntrospectionType = typeof IntrospectionTypeSchema.Type;
175export type IntrospectionSchema = (typeof IntrospectionResultSchema.Type)["__schema"];
176export type IntrospectionResult = typeof IntrospectionResultSchema.Type;
127 177
128// ---------------------------------------------------------------------------178// ---------------------------------------------------------------------------
129// Introspect a GraphQL endpoint179// Introspect a GraphQL endpoint
@@ -162,9 +212,9 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 32 unmodified lines
162 const response = yield* client.execute(request).pipe(212 const response = yield* client.execute(request).pipe(
163 Effect.tapCause((cause) => Effect.logError("graphql introspection request failed", cause)),213 Effect.tapCause((cause) => Effect.logError("graphql introspection request failed", cause)),
164 Effect.mapError(214 Effect.mapError(
165 (err) =>215 () =>
166 new GraphqlIntrospectionError({216 new GraphqlIntrospectionError({
167 message: `Failed to reach GraphQL endpoint: ${err.message}`,217 message: "Failed to reach GraphQL endpoint",
168 }),218 }),
169 ),219 ),
170 );220 );
@@ -187,7 +237,14 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 16 unmodified lines
187 ),237 ),
188 );238 );
189 239
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 );
191 248
192 if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {249 if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) {
193 return yield* new GraphqlIntrospectionError({250 return yield* new GraphqlIntrospectionError({
@@ -211,18 +268,12 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( · 17 unmodified lines
211export const parseIntrospectionJson = (268export const parseIntrospectionJson = (
212 text: string,269 text: string,
213): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> =>270): Effect.Effect<IntrospectionResult, GraphqlIntrospectionError> =>
214 Effect.try({271 Schema.decodeUnknownEffect(Schema.fromJsonString(IntrospectionJsonSchema))(text).pipe(
215 try: () => {272 Effect.map((parsed) => ("data" in parsed ? parsed.data : parsed)),
216 const parsed = JSON.parse(text);273 Effect.mapError(
217 // Accept both { data: { __schema } } and { __schema } formats274 () =>
218 const result = parsed.data ?? parsed;275 new GraphqlIntrospectionError({
219 if (!result.__schema) {276 message: "Failed to parse introspection JSON",
220 throw new Error("Missing __schema in introspection JSON");277 }),
221 }278 ),
222 return result as IntrospectionResult;279 );
223 },
224 catch: (err) =>
225 new GraphqlIntrospectionError({
226 message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`,
227 }),
228 });
packages/plugins/graphql/src/sdk/invoke.ts+1−10
@@ -114,7 +114,7 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( · 113 unmodified lines
114 Effect.mapError(114 Effect.mapError(
115 (err) =>115 (err) =>
116 new GraphqlInvocationError({116 new GraphqlInvocationError({
117 message: `GraphQL request failed: ${err.message}`,117 message: "GraphQL request failed",
118 statusCode: Option.none(),118 statusCode: Option.none(),
119 cause: err,119 cause: err,
120 }),120 }),
@@ -159,15 +159,6 @@ export const invokeWithLayer = ( · 38 unmodified lines
159) =>159) =>
160 invoke(operation, args, endpoint, resolvedHeaders, resolvedQueryParams).pipe(160 invoke(operation, args, endpoint, resolvedHeaders, resolvedQueryParams).pipe(
161 Effect.provide(httpClientLayer),161 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 ),
171 Effect.withSpan("plugin.graphql.invoke", {162 Effect.withSpan("plugin.graphql.invoke", {
172 attributes: {163 attributes: {
173 "plugin.graphql.endpoint": endpoint,164 "plugin.graphql.endpoint": endpoint,