Skip to content

Commit

Permalink
GET method for ApolloSchemaDownloader (#2010)
Browse files Browse the repository at this point in the history
* GET method for ApolloSchemaDownloader
* Minor improvements to HTTP method enum
* Remove ApolloSchemaDownload scope from name
* Add documentation
* Add HTTP method string constants as output
* Add error for unsupported HTTP method when using Apollo Registry
* Move HTTP method support into DownloadMethod
* Build requests based on DownloadMethod
* Add tests for DownloadMethod HTTP method configurations
* Clean up and clarify documentation
* Add associated values to URL-related errors

Co-authored-by: Calvin Cestari <calvin.cestari@gmail.com>
  • Loading branch information
mikepitre and calvincestari authored Nov 8, 2021
1 parent 9cab672 commit 3bfde02
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 42 deletions.
25 changes: 22 additions & 3 deletions Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct ApolloSchemaDownloadConfiguration {
/// The Apollo Schema Registry, which serves as a central hub for managing your graph.
case apolloRegistry(_ settings: ApolloRegistrySettings)
/// GraphQL Introspection connecting to the specified URL.
case introspection(endpointURL: URL)
case introspection(endpointURL: URL, httpMethod: HTTPMethod = .POST)

public struct ApolloRegistrySettings: Equatable {
/// The API key to use when retrieving your schema from the Apollo Registry.
Expand All @@ -33,11 +33,30 @@ public struct ApolloSchemaDownloadConfiguration {
self.variant = variant
}
}

/// The HTTP request method. This is an option on Introspection schema downloads only. Apollo Registry downloads are always
/// POST requests.
public enum HTTPMethod: Equatable, CustomStringConvertible {
/// Use POST for HTTP requests. This is the default for GraphQL.
case POST
/// Use GET for HTTP requests with the GraphQL query being sent in the query string parameter named in
/// `queryParameterName`.
case GET(queryParameterName: String)

public var description: String {
switch self {
case .POST:
return "POST"
case .GET:
return "GET"
}
}
}

public static func == (lhs: DownloadMethod, rhs: DownloadMethod) -> Bool {
switch (lhs, rhs) {
case (.introspection(let lhsURL), introspection(let rhsURL)):
return lhsURL == rhsURL
case (.introspection(let lhsURL, let lhsHTTPMethod), .introspection(let rhsURL, let rhsHTTPMethod)):
return lhsURL == rhsURL && lhsHTTPMethod == rhsHTTPMethod
case (.apolloRegistry(let lhsSettings), .apolloRegistry(let rhsSettings)):
return lhsSettings == rhsSettings
default:
Expand Down
132 changes: 93 additions & 39 deletions Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public struct ApolloSchemaDownloader {
case couldNotExtractSDLFromRegistryJSON
case couldNotCreateSDLDataToWrite(schema: String)
case couldNotConvertIntrospectionJSONToSDL(underlying: Error)
case couldNotCreateURLComponentsFromEndpointURL(url: URL)
case couldNotGetURLFromURLComponents(components: URLComponents)

public var errorDescription: String? {
switch self {
Expand All @@ -29,7 +31,11 @@ public struct ApolloSchemaDownloader {
case .couldNotCreateSDLDataToWrite(let schema):
return "Could not convert SDL schema into data to write to the filesystem. Schema: \(schema)"
case .couldNotConvertIntrospectionJSONToSDL(let underlying):
return "Could not convert downloaded introspection JSON into SDL format. Underlying error: \(underlying)"
return "Could not convert downloaded introspection JSON into SDL format. Underlying error: \(underlying)"
case .couldNotCreateURLComponentsFromEndpointURL(let url):
return "Could not create URLComponents from \(url) for Introspection."
case .couldNotGetURLFromURLComponents(let components):
return "Could not get URL from \(components)."
}
}
}
Expand All @@ -43,18 +49,36 @@ public struct ApolloSchemaDownloader {
try FileManager.default.apollo.createContainingFolderIfNeeded(for: configuration.outputURL)

switch configuration.downloadMethod {
case .introspection(let endpointURL):
try self.downloadViaIntrospection(from: endpointURL, configuration: configuration)
case .introspection(let endpointURL, let httpMethod):
try self.downloadViaIntrospection(from: endpointURL, httpMethod: httpMethod, configuration: configuration)
case .apolloRegistry(let settings):
try self.downloadFromRegistry(with: settings, configuration: configuration)
}
}

private static func request(url: URL,
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader],
bodyData: Data? = nil) -> URLRequest {

var request = URLRequest(url: url)

request.addValue("application/json", forHTTPHeaderField: "Content-Type")
for header in headers {
request.addValue(header.value, forHTTPHeaderField: header.key)
}

request.httpMethod = String(describing: httpMethod)
request.httpBody = bodyData

return request
}

// MARK: - Schema Registry

static let RegistryEndpoint = URL(string: "https://graphql.api.apollographql.com/api/graphql")!

private static let RegistryDownloadQuery = """
static let RegistryDownloadQuery = """
query DownloadSchema($graphID: ID!, $variant: String!) {
service(id: $graphID) {
variant(name: $variant) {
Expand All @@ -74,27 +98,9 @@ public struct ApolloSchemaDownloader {

CodegenLogger.log("Downloading schema from registry", logLevel: .debug)

var variables = [String: String]()
variables["graphID"] = settings.graphID

if let variant = settings.variant {
variables["variant"] = variant
}

let body = UntypedGraphQLRequestBodyCreator.requestBody(for: self.RegistryDownloadQuery,
variables: variables,
operationName: "DownloadSchema")

var urlRequest = URLRequest(url: self.RegistryEndpoint)
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue(settings.apiKey, forHTTPHeaderField: "x-api-key")
for header in configuration.headers {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
}
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.sortedKeys])
let urlRequest = try registryRequest(with: settings, headers: configuration.headers)
let jsonOutputURL = configuration.outputURL.apollo.parentFolderURL().appendingPathComponent("registry_response.json")

try URLDownloader().downloadSynchronously(with: urlRequest,
to: jsonOutputURL,
timeout: configuration.downloadTimeout)
Expand All @@ -104,6 +110,31 @@ public struct ApolloSchemaDownloader {
CodegenLogger.log("Successfully downloaded schema from registry", logLevel: .debug)
}

static func registryRequest(with settings: ApolloSchemaDownloadConfiguration.DownloadMethod.ApolloRegistrySettings,
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader]) throws -> URLRequest {

var variables = [String: String]()
variables["graphID"] = settings.graphID
if let variant = settings.variant {
variables["variant"] = variant
}

let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: self.RegistryDownloadQuery,
variables: variables,
operationName: "DownloadSchema")
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])

var allHeaders = headers
allHeaders.append(ApolloSchemaDownloadConfiguration.HTTPHeader(key: "x-api-key", value: settings.apiKey))

let urlRequest = request(url: self.RegistryEndpoint,
httpMethod: .POST,
headers: allHeaders,
bodyData: bodyData)

return urlRequest
}

static func convertFromRegistryJSONToSDLFile(jsonFileURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
let jsonData: Data

Expand Down Expand Up @@ -143,7 +174,7 @@ public struct ApolloSchemaDownloader {

// MARK: - Schema Introspection

private static let IntrospectionQuery = """
static let IntrospectionQuery = """
query IntrospectionQuery {
__schema {
queryType { name }
Expand Down Expand Up @@ -235,21 +266,13 @@ public struct ApolloSchemaDownloader {
"""


static func downloadViaIntrospection(from endpointURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
static func downloadViaIntrospection(from endpointURL: URL,
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
configuration: ApolloSchemaDownloadConfiguration) throws {

CodegenLogger.log("Downloading schema via introspection from \(endpointURL)", logLevel: .debug)

var urlRequest = URLRequest(url: endpointURL)
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")

for header in configuration.headers {
urlRequest.addValue(header.value, forHTTPHeaderField: header.key)
}

let body = UntypedGraphQLRequestBodyCreator.requestBody(for: self.IntrospectionQuery,
variables: nil,
operationName: "IntrospectionQuery")
urlRequest.httpMethod = "POST"
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: body, options: [.sortedKeys])
let urlRequest = try introspectionRequest(from: endpointURL, httpMethod: httpMethod, headers: configuration.headers)
let jsonOutputURL = configuration.outputURL.apollo.parentFolderURL().appendingPathComponent("introspection_response.json")

try URLDownloader().downloadSynchronously(with: urlRequest,
Expand All @@ -260,7 +283,38 @@ public struct ApolloSchemaDownloader {

CodegenLogger.log("Successfully downloaded schema via introspection", logLevel: .debug)
}


static func introspectionRequest(from endpointURL: URL,
httpMethod: ApolloSchemaDownloadConfiguration.DownloadMethod.HTTPMethod,
headers: [ApolloSchemaDownloadConfiguration.HTTPHeader]) throws -> URLRequest {
let urlRequest: URLRequest

switch httpMethod {
case .POST:
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: self.IntrospectionQuery,
variables: nil,
operationName: "IntrospectionQuery")
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])
urlRequest = request(url: endpointURL,
httpMethod: httpMethod,
headers: headers,
bodyData: bodyData)

case .GET(let queryParameterName):
guard var components = URLComponents(url: endpointURL, resolvingAgainstBaseURL: true) else {
throw SchemaDownloadError.couldNotCreateURLComponentsFromEndpointURL(url: endpointURL)
}
components.queryItems = [URLQueryItem(name: queryParameterName, value: IntrospectionQuery)]

guard let url = components.url else {
throw SchemaDownloadError.couldNotGetURLFromURLComponents(components: components)
}
urlRequest = request(url: url, httpMethod: httpMethod, headers: headers)
}

return urlRequest
}

static func convertFromIntrospectionJSONToSDLFile(jsonFileURL: URL, configuration: ApolloSchemaDownloadConfiguration) throws {
let frontend = try ApolloCodegenFrontend()
let schema: GraphQLSchema
Expand Down
87 changes: 87 additions & 0 deletions Tests/ApolloCodegenTests/ApolloSchemaInternalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,92 @@ class ApolloSchemaInternalTests: XCTestCase {

XCTAssertEqual(downloadConfiguration.outputURL, codegenOptions.urlToSchemaFile)
}

func testRequest_givenIntrospectionGETDownload_shouldOutputGETRequest() throws {
let url = ApolloTestSupport.TestURL.mockServer.url
let queryParameterName = "customParam"
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
.init(key: "key1", value: "value1"),
.init(key: "key2", value: "value2")
]

let request = try ApolloSchemaDownloader.introspectionRequest(from: url,
httpMethod: .GET(queryParameterName: queryParameterName),
headers: headers)

XCTAssertEqual(request.httpMethod, "GET")
XCTAssertNil(request.httpBody)

XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
for header in headers {
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
}

var components = URLComponents(url: url, resolvingAgainstBaseURL: true)
components?.queryItems = [URLQueryItem(name: queryParameterName, value: ApolloSchemaDownloader.IntrospectionQuery)]

XCTAssertNotNil(components?.url)
XCTAssertEqual(request.url, components?.url)
}

func testRequest_givenIntrospectionPOSTDownload_shouldOutputPOSTRequest() throws {
let url = ApolloTestSupport.TestURL.mockServer.url
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
.init(key: "key1", value: "value1"),
.init(key: "key2", value: "value2")
]

let request = try ApolloSchemaDownloader.introspectionRequest(from: url, httpMethod: .POST, headers: headers)

XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, url)

XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
for header in headers {
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
}

let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: ApolloSchemaDownloader.IntrospectionQuery,
variables: nil,
operationName: "IntrospectionQuery")
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])

XCTAssertEqual(request.httpBody, bodyData)
}

func testRequest_givenRegistryDownload_shouldOutputPOSTRequest() throws {
let apiKey = "custom-api-key"
let graphID = "graph-id"
let variant = "a-variant"
let headers: [ApolloSchemaDownloadConfiguration.HTTPHeader] = [
.init(key: "key1", value: "value1"),
.init(key: "key2", value: "value2"),
]

let request = try ApolloSchemaDownloader.registryRequest(with: .init(apiKey: apiKey,
graphID: graphID,
variant: variant),
headers: headers)

XCTAssertEqual(request.httpMethod, "POST")
XCTAssertEqual(request.url, ApolloSchemaDownloader.RegistryEndpoint)

XCTAssertEqual(request.allHTTPHeaderFields?["Content-Type"], "application/json")
XCTAssertEqual(request.allHTTPHeaderFields?["x-api-key"], apiKey)
for header in headers {
XCTAssertEqual(request.allHTTPHeaderFields?[header.key], header.value)
}

let variables: [String: String] = [
"graphID": graphID,
"variant": variant
]
let requestBody = UntypedGraphQLRequestBodyCreator.requestBody(for: ApolloSchemaDownloader.RegistryDownloadQuery,
variables: variables,
operationName: "DownloadSchema")
let bodyData = try JSONSerialization.data(withJSONObject: requestBody, options: [.sortedKeys])

XCTAssertEqual(request.httpBody, bodyData)
}
}

0 comments on commit 3bfde02

Please sign in to comment.