Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GET method for ApolloSchemaDownloader #2010

Merged
merged 8 commits into from
Nov 8, 2021
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)
}
}