Using Apollo with AppSync directly on iOS

I just came across this post and solved this issue so I will share some code for others that run into it. Took me a while of trial and error to piece it together. I am integrating with AppSync here via api-key auth. In the snippet below you can see the additional header I add.

To get it to work, create the transport / client as normal, but what is required is to setup a custom request body creator as mentioned above. You have to pass it to any transport you create. In my case this is for a AppSync websocket endpoint.

        // Create the websocket client
        let webSocketClient = WebSocket(request: request, protocol: .graphql_ws)
        
        // This custom body wrapper is required for AppSync's protocol
        let requestBody = AppSyncRequestBodyCreator()
        
        requestBody.authorization = authHeaders
        
        let webSocketTransport = WebSocketTransport(websocket: webSocketClient, requestBodyCreator: requestBody)
        
        let normalTransport = RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: store), endpointURL: normalUrl, additionalHeaders: ["x-api-key" : parsedResponse.data.config.appSyncKey])
        
        // Using a split transport allows us to handle both standard HTTP queries and websockets through the same client
        let splitTransport = SplitNetworkTransport(uploadingNetworkTransport: normalTransport, webSocketNetworkTransport: webSocketTransport)
        self.client = ApolloClient(networkTransport: splitTransport, store: self.store)

The code for the RequestBodyCreator:

/**
 Creates an AWS AppSync-compatible RequestBody for subscriptions.
 Credit to: https://community.apollographql.com/t/using-apollo-with-appsync-directly-on-ios/324/4
 */
public class AppSyncRequestBodyCreator : RequestBodyCreator {
    
    // Don't forget to to set these! This is required for the initial handshake.
    var authorization: [String : String]!
    
    public func requestBody<Operation>(for operation: Operation, sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool) -> GraphQLMap where Operation : GraphQLOperation {
        
        var body: GraphQLMap = [
            "variables": operation.variables,
            "operationName": operation.operationName,
        ]
        
        if sendOperationIdentifiers {
            guard let operationIdentifier = operation.operationIdentifier else {
                preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers")
            }
            
            body["id"] = operationIdentifier
        }
        
        if sendQueryDocument {
            body["query"] = operation.queryDocument
        }
        
        // The data portion of the body needs to have the query and variables as well.
        guard let data = try? JSONSerialization.data(withJSONObject: ["query": operation.queryDocument,
                                                                      "variables": operation.variables ?? [:]], options: .prettyPrinted) else {
            fatalError("Somehow the query and variables aren't valid JSON!")
        }
        let jsonString = String(data: data, encoding: .utf8)
        body["data"] = jsonString
        
        if autoPersistQuery {
            guard let operationIdentifier = operation.operationIdentifier else {
                preconditionFailure("To enable `autoPersistQueries`, Apollo types must be generated with operationIdentifiers")
            }
            
            body["extensions"] = [
                "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1],
                "authorization": authorization
            ]
        } else {
            body["extensions"] = ["authorization": authorization]
        }
        
        return body
    }
}
1 Like