| @@ -1,4 +1,4 @@ |
| 1 | import { Effect } from "effect"; | 1 | import { Effect, Schema } from "effect"; |
| 2 | import { HttpClient, HttpClientRequest } from "effect/unstable/http"; | 2 | import { HttpClient, HttpClientRequest } from "effect/unstable/http"; |
| 3 | | 3 | |
| 4 | import { GraphqlIntrospectionError } from "./errors"; | 4 | import { GraphqlIntrospectionError } from "./errors"; |
| @@ -81,49 +81,99 @@ const INTROSPECTION_QUERY = ` · 76 unmodified lines |
| 81 | // Introspection result types | 81 | // Introspection result types |
| 82 | // --------------------------------------------------------------------------- | 82 | // --------------------------------------------------------------------------- |
| 83 | | 83 | |
| 84 | export interface IntrospectionTypeRef { | 84 | const 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 | |
| 90 | export interface IntrospectionInputValue { | 90 | const 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 | | 96 | const IntrospectionTypeRef4 = Schema.Struct({ |
| 97 | export 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 | } | 102 | const IntrospectionTypeRef3 = Schema.Struct({ |
| 103 | | 103 | kind: Schema.String, |
| 104 | export 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 | | 108 | const IntrospectionTypeRef2 = Schema.Struct({ |
| 109 | export 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; | 114 | const 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), |
| 118 | export interface IntrospectionSchema { | 118 | }); |
| 119 | readonly queryType: { readonly name: string } | null; | 119 | |
| 120 | readonly mutationType: { readonly name: string } | null; | 120 | const IntrospectionInputValueSchema = Schema.Struct({ |
| 121 | readonly types: readonly IntrospectionType[]; | 121 | name: Schema.String, |
| 122 | } | 122 | description: Schema.NullOr(Schema.String), |
| 123 | | 123 | type: IntrospectionTypeRefSchema, |
| 124 | export interface IntrospectionResult { | 124 | defaultValue: Schema.NullOr(Schema.String), |
| 125 | readonly __schema: IntrospectionSchema; | 125 | }); |
| 126 | } | 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; |
| 127 | | 177 | |
| 128 | // --------------------------------------------------------------------------- | 178 | // --------------------------------------------------------------------------- |
| 129 | // Introspect a GraphQL endpoint | 179 | // 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 |
| 211 | export const parseIntrospectionJson = ( | 268 | export 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 } formats | 274 | () => |
| 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 | }); | | |