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

Feature: Auto Persisted Queries with CDN Support #608

Closed

Conversation

Codebear98
Copy link

@Codebear98 Codebear98 commented Jul 2, 2019

Usage

let networkTransport = HTTPNetworkTransport(
      url: url,
      configuration: sessionConfiguration,
      useGETForQueries: false,
      enableAutoPersistedQueries: true,
      useHttpGetMethodForPersistedQueries: true)

let client = ApolloClient(networkTransport: networkTransport)

Features

useGETMethodForPersistedQueries=true is aligning with apollo-android #1376

  1. it sends GetMethod for the 1st Request.
  2. If server dose not have cache or not support APQs with error message PersistedQueryNotFound or PersistedQueryNotSupported
  3. Then it fallbacks to retry with POST Request (retry with GET when useGETForQueries=true) and includes QueryDocument for registering APQs.

useGETMethodForPersistedQueries is introduced due to an error exceeds limit on url query string may occur on some sever when query string exceeds 2k limit with large amount of data such as QueryDoucment.

Limitation

  1. To enable APQs,sha256hash of QueryDocument is required for each QueryOperation.
    You need to Modify Apollo runscript in YOUR PROJECT to also generate operationIdentifier (sha256hash) to your API.swift (please see below)

  2. Must use Apollo 1.9.2, since there is a bug in Apollo-tooling while generating operationIdentifier
    The Query and hash does not match when the Query contains fragment. It's due to an extra \n added in QueryDocument for hashing. But the \n is missing in Swift CodeGen.
    operationId is invalid for swift codegen apollo-tooling#1362

There is a hacking workaround in HTTPNetworkTransport.swift line212, It adds back the \n to the QueryDocument.

if sendQueryDocument {
    // TODO: This work-around fix "operationId is invalid for swift codegen" (https://github.com/apollographql/apollo-tooling/issues/1362), please remove this work-around after it's fixed.
    let modifiedQuery = operation.queryDocument.replacingOccurrences(of: "fragment", with: "\nfragment")
    payload["query"] = modifiedQuery
  }

Apollo runscript
add --operationIdsPath=operationIdsPath for swift codegen


# Type a script or drag a script file from your workspace to insert its path.
APOLLO_FRAMEWORK_PATH=$(eval find $FRAMEWORK_SEARCH_PATHS -name "Apollo.framework" -maxdepth 1 -print | head -n 1)

if [ -z "$APOLLO_FRAMEWORK_PATH" ]; then
echo "error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project."
exit 1
fi

# Download schema.json first 
# apollo schema:download --endpoint=http... --header="apikey: ..."
cd "${SRCROOT}/${TARGET_NAME}/DataSource/GraphQL"
$APOLLO_FRAMEWORK_PATH/check-and-run-apollo-cli.sh codegen:generate --passthroughCustomScalars --queries="$(find . -name '*.graphql')" --operationIdsPath=operationIdsPath --schema=schema.json Generated/GraphQLAPI.swift

@designatednerd
Copy link
Contributor

@Codebear98 we've got some changes in progress with this with #599 and #602 - you may want to hold off until those get merged!

@Codebear98
Copy link
Author

@Codebear98 we've got some changes in progress with this with #599 and #602 - you may want to hold off until those get merged!

cool, make sense for #599, probably can align with apollo-android.

@Codebear98 Codebear98 force-pushed the support_get_apqs branch 2 times, most recently from 9748016 to 9e69402 Compare July 4, 2019 11:00
@Codebear98
Copy link
Author

PR updated following the design of #599 and #602 @designatednerd please take a look, thanks!

Copy link
Contributor

@designatednerd designatednerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this has a lot of the same stuff in it as #583 - let's get that one dealt with first. I'm going to mark this as changes requested until that gets merged to avoid accidentally merging this one.

Sources/Apollo/GraphQLGETTransformer.swift Show resolved Hide resolved
Sources/Apollo/HTTPNetworkTransport.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/HttpNetworkTransportTests.swift Outdated Show resolved Hide resolved
Tests/ApolloCacheDependentTests/StarWarsServerTests.swift Outdated Show resolved Hide resolved
Sources/Apollo/HTTPNetworkTransport.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/HttpNetworkTransportTests.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/HttpNetworkTransportTests.swift Outdated Show resolved Hide resolved
Sources/Apollo/HTTPNetworkTransport.swift Outdated Show resolved Hide resolved
Sources/Apollo/HTTPNetworkTransport.swift Outdated Show resolved Hide resolved
Tests/ApolloTests/HTTPTransportTests.swift Outdated Show resolved Hide resolved
@designatednerd
Copy link
Contributor

One thing that @SirensOfTitan pointed out when I closed his PR is that this ties APQ to the HTTPNetworkTransport - it would probably be good to figure out a way to make this also work for the WebSocketTransport as well.

@Codebear98
Copy link
Author

ould probably be good to figure out a way to make this also work for the WebSocketTransport as well.

Probably refactor to decouple the request payload part out from NetworkTransport?
I'm not familiar with ApolloWebSocketClient, does it handle the same way?
Can also update the WebSocketTransport to currently progress.

Henry Hong added 4 commits July 16, 2019 18:55
2. remove `_` on private variable
3. add guard clause to optional variable in HttpNetworkTransportTests
4. removed redundant parameter in StarWarsServerTests
5. change // Mark to // MARK
6. single line for method declaration
7. removed redundant code in HTTPNetworkTransport
9. update switch style
Sources/Apollo/GraphQLGETTransformer.swift Show resolved Hide resolved
Apollo.xcodeproj/xcshareddata/xcschemes/GitHubAPI.xcscheme Outdated Show resolved Hide resolved
Sources/Apollo/GraphQLHTTPResponseError.swift Outdated Show resolved Hide resolved
let url: URL
let session: URLSession
var session: URLSession
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this now need to be a var?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, It's used to assign MockSession to intercept the Request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok - let's go with this for now. I'm planning to get some stuff allowing a URL session to be passed in with an initializer soon.

Sources/Apollo/HTTPNetworkTransport.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift Outdated Show resolved Hide resolved
Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift Outdated Show resolved Hide resolved
Tests/ApolloCacheDependentTests/StarWarsServerTests.swift Outdated Show resolved Hide resolved
Tests/ApolloTests/GETTransformerTests.swift Outdated Show resolved Hide resolved
let response = GraphQLResponse(operation: operation, body: body)
completionHandler(response, nil)
if self.enableAutoPersistedQueries,
let error = body["errors"] as? [JSONObject],
Copy link
Contributor

@RolandasRazma RolandasRazma Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is getting bit messy here... How about creating GraphQLResponse (else path) and adding method to it to get errors. That way you don't need to know here much about JSONObject and GraphQLResponse already knows about it.

let response = GraphQLResponse(operation: operation, body: body)

if response.errors.contain(...) {

} else {
   completionHandler(response, nil)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, will update accordingly!

Henry Hong added 2 commits July 19, 2019 16:43
renamed useHttpGetMethodForPersistedQueries to useGETForPersistedQueryRetry
un-shared test projects
fixed grammer typo in GraphQLHTTPResponseError
updated to use XCTAssertEqual

var payload: GraphQLMap = [:]

if autoPersistQueries {
guard let operationIdentifier = operation.operationIdentifier else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this should be preconditionFailure. You might want persist queries for some but not all request. Wouldn't it be better to just do normal query if operation.operationIdentifier is nil

Copy link
Author

@Codebear98 Codebear98 Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing.
But u can't specify which request to use persistQueries but not all.
And all or none operationIdentifier will be generated according to Apollo run-script.
if operationIdentifier missing and you wanna enable APQs, definitely indicate there is an issue in Apollo Codegen script.

Did I miss something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--operationIdsPath is path to JSON file that is tooling dependant and/or can be modified as you see fit

Copy link
Author

@Codebear98 Codebear98 Jul 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question here is Do we need to support autoPersistQueries just for some requests but not for all?
As the autoPersistQueries seems network traffic optimisation like CDN caching which should be applying to all or none because it depends server side ability to support or not.

I just worry if we don't preconditionFailure for the pre-gen code that's not fulfilling the APQs requirement (but dev opt to turn on APQs). Developers will not be able to get alert about the run script missing operationIdsPath until they look into the traffic logging and find out APQs isn't in place.

In addition, I think the JSON file will not affect the code gen? I mean even you modify the JSON file, it won't affect operation.operationIdentifier in the generated Code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC the operation identifier is based on the hash of the body, so it shouldn't change based on the location of the codegen.

Personally I'm good with leaving this as a precondition failure - @Codebear98 is correct, if someone wants APQ's and hasn't set up generating operation IDs, we want this to fail loud, hard, and early.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

guard
let delegate = self.delegate,
let retrier = delegate as? HTTPNetworkTransportRetryDelegate else {
// retryByDefault: still retry if delegate is absent, otherwise repect the value from delegate
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment should now be removed

_ = self.send(operation: operation, retryFor: error, completionHandler: completionHandler)
})
} else if retryByDefault {
_ = self.send(operation: operation, retryFor: error, completionHandler: completionHandler)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still have this concern

request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(operation.operationIdentifier, forHTTPHeaderField: "X-APOLLO-OPERATION-ID")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you may need to either rebase or merge in from master, there's some stuff there around this that's checking whether there's an operation ID before sending this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, and I would also add operationName in APQs request body, as it's available now thanks for 2.16 tooling upgrade!


class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{
func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedError error: Error, for request: URLRequest, response: URLResponse?, retryHandler: @escaping (Bool) -> Void) {
retryHandler(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to check that the received error is of the correct type? Otherwise this could lead to an infinite loop.


import Foundation

public final class MockURLSession: URLSession {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does all this need to be public?

XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D")
let queryString = url?.absoluteString == "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D"

XCTAssertTrue(queryString)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this changed?

XCTFail("variables should not nil")
return
}
XCTAssertEqual(variables, "{\"episode\":null}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can also check variables.keys.contains("episode") and then check that the value for that is null rather than checking the raw string.

@@ -109,6 +299,6 @@ class GETTransformerTests: XCTestCase {
let transformer = GraphQLGETTransformer(body: body, url: self.url)

let url = transformer.createGetURL()
XCTAssertNil(url)
XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?variables=%7B%22episode%22:%22EMPIRE%22%7D")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I wound up deleting this test in master, you're welcome to as well.

@designatednerd
Copy link
Contributor

@Codebear98 Do you have interest in continuing this, or do you mind if I take this PR over?

@Codebear98
Copy link
Author

@designatednerd sorry, I don’t wanna delay the progress, please take it over if u would, thanks!

@designatednerd
Copy link
Contributor

This feature can now be followed at #767

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants