diff --git a/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift b/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift index 0ce1c7b..496a104 100644 --- a/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift +++ b/Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift @@ -40,9 +40,16 @@ extension GraphQLHandler { variables: variables ) + let documentAST: Document let operationType: OperationType do { - operationType = try graphQLRequest.operationType() + documentAST = try parse( + source: Source(body: graphQLRequest.query, name: graphQLRequest.operationName ?? "") + ) + operationType = try Self.operationTypeOf( + document: documentAST, + named: graphQLRequest.operationName + ) } catch { // Indicates a request parsing error throw HTTPError(.badRequest, message: error.localizedDescription) @@ -60,7 +67,9 @@ extension GraphQLHandler { ) let graphQLContext = try await computeContext(graphQLContextComputationInputs) let result = await execute( - graphQLRequest: graphQLRequest, + documentAST: documentAST, + operationName: graphQLRequest.operationName, + variables: graphQLRequest.variables, context: graphQLContext, additionalValidationRules: config.additionalValidationRules ) @@ -100,6 +109,17 @@ extension GraphQLHandler { throw HTTPError(.unsupportedMediaType) } + let documentAST: Document + do { + documentAST = try parse( + source: Source(body: graphQLRequest.query, name: graphQLRequest.operationName ?? "") + ) + } catch let error as GraphQLError { + // Indicates a request parsing error + let result = GraphQLResult(data: nil, errors: [error]) + return try encodeResponse(result: result, request: request, context: context) + } + let graphQLContextComputationInputs = GraphQLContextComputationInputs< Context, WebSocketInitResult >( @@ -110,7 +130,9 @@ extension GraphQLHandler { ) let graphQLContext = try await computeContext(graphQLContextComputationInputs) let result = await execute( - graphQLRequest: graphQLRequest, + documentAST: documentAST, + operationName: graphQLRequest.operationName, + variables: graphQLRequest.variables, context: graphQLContext, additionalValidationRules: config.additionalValidationRules ) @@ -118,24 +140,45 @@ extension GraphQLHandler { } private func execute( - graphQLRequest: GraphQLRequest, + documentAST: Document, + operationName: String?, + variables: [String: Map], context: GraphQLContext, additionalValidationRules: [@Sendable (ValidationContext) -> Visitor] ) async -> GraphQLResult { // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#validation + + // Validate schema + do { + let schemaValidationErrors = try validateSchema(schema: schema) + guard schemaValidationErrors.isEmpty else { + return GraphQLResult(errors: schemaValidationErrors) + } + } catch { + return GraphQLResult(errors: [GraphQLError(error)]) + } + + // Validate request let validationRules = GraphQL.specifiedRules + additionalValidationRules + let validationErrors = validate( + schema: schema, + ast: documentAST, + rules: validationRules + ) + guard validationErrors.isEmpty else { + return GraphQLResult(errors: validationErrors) + } // https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#execution let result: GraphQLResult do { - result = try await graphql( + result = try await GraphQL.execute( schema: schema, - request: graphQLRequest.query, + documentAST: documentAST, rootValue: rootValue, context: context, - variableValues: graphQLRequest.variables, - operationName: graphQLRequest.operationName, - validationRules: validationRules + variableValues: variables, + operationName: operationName ) } catch let error as GraphQLError { // This indicates a request parsing error @@ -205,4 +248,26 @@ extension GraphQLHandler { MediaType(from: segment.trimmingCharacters(in: .whitespaces)) } } + + private static func operationTypeOf( + document: Document, + named operationName: String? = nil + ) throws -> OperationType { + let operationDefinitions = document.definitions.filter { operation in + operation.kind == .operationDefinition + }.compactMap { operation in + operation as? OperationDefinition + } + + // We don't have to validate the document and check for multiple/missing operations here + // That will be done upon execution. + let operationMatch = operationDefinitions.first { operation in + if let operationName { + return operationName == operation.name?.value + } else { + return true + } + } + return operationMatch?.operation ?? .query + } }