How to wrap ApolloClient in a network facade while GraphQLOperation has associatedType requirements

Hello,

I am trying to use Apollo in my iOS Network Stack next to my current REST Stack in order to implement a migration while the API are being build.

However I am getting stuck due to the way the SDK is built and I need some help to understand if there any way to use it the way I would like to or not.

I will skip the details of protocol Query and Mutation because this is an additional specification that does not impact my problem at the moment.

The main entry point to send a request via Transport is defined in the SDK as

func send<Operation: GraphQLOperation>(operation: Operation,
                                         cachePolicy: CachePolicy,
                                         contextIdentifier: UUID?,
                                         callbackQueue: DispatchQueue,
                                         completionHandler: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) -> Cancellable

where a GraphQLOperation is defined as

public protocol GraphQLOperation: class {
   ...
  associatedtype Data: GraphQLSelectionSet
}

and GraphQLSelectionSet is a protocol defining basically a Json Response in a dictionary.


I am using the code generation tool to produce my queries.

I now have concrete class types of GraphQLOperation.
they all define a different

public struct Data: GraphQLSelectionSet {

which is the way the associatedType is satisfied (instead of being a typealias for example)


My problem is that I want to use a Generic GraphQLOperation without the concrete type in order to have one single entry point in the network.

I have a network facade with basically this entry point:

public func make<T: Returnable>(request: T, completion: @escaping (Result<T.ReturnType, ServiceError>) -> Void) {

where Returnable is a protocol I use to define the App model which should be returned for each request.

What I want to do is being able from my app to do

let request = DataRequest.cars(for "xyz")
network.make(request) { carsResults in }

The Network facade will use the DataRequest content to map either to a REST request (this is already working) or to a GraphQLRequest,
forward the request to the correct network client implementation, parse the network response in a network model (eg: a specific GraphQLResult), then map this network model to the external (usually 1to1 App Model specified by the Returnable protocol) and send it to the completion block.

This ensure the app can send requests without knowing the service which is used and receive the same result, whatever is the state of the network migration.


This works for the REST part of the API.

I cannot make this work for the Apollo Part, because there is no way for me to have a mapping between my own Enum of DataRequests and GraphQLOperations

simplified example (I have 2 layers of mapping to choose the service instead, but that is out of scope of the question)

enum DataRequest {
      case cars(for user: String)
      case trains(for user: String)

      var operation: GraphQLOperation {
            switch self {
                  case .cars(let id):
                        return AllCarsQuery(id: id)

                  case .trains(let id):
                        return AllTrainsQuery(id: id)
            }
      }
}

This cannot be done in any way, because GraphQLOperation has associated type constraint.

I tried solving this problem with type erasure, opaque type etc… but in the end there is no way to provide a generic instance of a request to a ApolloClient

Do you have any suggestion to solve this problem or is the Apollo Framework built only for using concrete classes?

Basically is it ONLY possible to do

ApolloClient.fetch(AllCarsQuery("id"))

or there is any way to parametrize this?

ApolloClient.fetch(genericQuery)

If the concrete type is mandatory then this would be extremely limiting for me in terms of using the SDK.
I also considered subclassing the Transport and Client but unfortunately the open methods are not enough for me to override the problem.
nor are the exposed interfaces as they require to use GraphQLOperation, that is the problem in itself as it has associated type requirements


One thing I would consider is getting back a result type of <Any, Error> in order to externally map the return type myself (since I know it from the request and the compiler should be happy about operation.Data.Type)

But in the current SDK state there seems to be no other way than to always provide a Concrete class (which I would like to do with a mapper and not directly)

example. provided this code:

var operation: Any {
            AllCommentsQuery(collectionId: "asd")
        }

it’s only possible to cast operation back to AllCommentsQuery
There is no way to cast it to a protocol that the SDK would accept.

Everything is based on the associatedType of the concrete class.
Hence There seems to be no way to hide the implementation details of GraphQL to the application layer.

a similar question would be: How would you make a factory for Query or Mutations based on a enum?

error from opaque factory:

var operation: some GraphQLOperation {
            AllCommentsQuery(collectionId: "asd")
        }

graphQLNetwork.send(operation: request.operation) { result in }

Member 'operation' cannot be used on value of protocol type 'GraphQLRequest'; use a generic constraint instead


Note that type erasure cannot be achieved because the Data associated type of GraphQLOperation is not used as part of the protocol

Thanks for the help

1 Like

The issue here is that the ApolloClient needs to know the type of the returned data structure in order to parse and validate the response and construct the returned data struct.

I’m not sure why that’s the case. This should compile just fine:

class AnyGraphQLOperation<U: GraphQLSelectionSet>: GraphQLOperation {
    typealias Data = U

    init<Base: GraphQLOperation>(base : Base) where Base.Data == U {
        operationDefinition = base.operationDefinition
        operationType = base.operationType
        operationIdentifier = base.operationIdentifier
        operationName = base.operationName
        queryDocument = base.queryDocument
        variables = base.variables
    }

    var operationType: GraphQLOperationType
    var operationDefinition: String
    var operationIdentifier: String?
    var operationName: String
    var queryDocument: String
    var variables: GraphQLMap?
}

But even still, I don’t think that is going to solve your problem.


You’re definitely not going to be able to have a property that just returns an abstract GraphQLOperation on your DataRequest enum.

Unless I’m misunderstanding something here, even with your networking abstraction, you’ll need to know the return type. It looks like in your full code DataRequest conforms to Returnable, which looks like it has an associatedType ReturnType. So you’re going to need to have a definition at all times of what that return type is.

Because your requests (and likewise, Apollo’s operations) each have a specified ReturnType, I don’t think you’re going to be well served with using an enum for this. You’ll need each data request entity to be either
A) A generic struct with the ReturnType specified
B) A concrete struct of its own that conforms to Returnable

What about something like this?

protocol Returnable {
  associatedtype ReturnType
}

struct DataRequest<R: GraphQLSelectionSet>: Returnable {
  typealias ReturnType = R

  let operation: AnyGraphQLOperation<R>

  private init<O: GraphQLOperation>(operation: O) where O.Data == ReturnType {
    self.operation = AnyGraphQLOperation(base: operation)
  }

  static func cars(for user: String) -> Self where R == AllCarsQuery.Data { 
// Where clause is needed because compiler can't infer generic type for static methods.
      return self.init(operation: AllCarsQuery(id: user))
  }

  static func trains(for user: String) -> Self where R == AllTrainsQuery.Data {
    return self.init(operation: AllTrainsQuery(id: user))
  }
}

// Mock Objects I created just to test that this works (these would be coming from Apollo's generated code).

class AllCarsQuery: GraphQLOperation {
  typealias Data = MockSelectionSet

  init(id: String) {
  }
  
  var operationType: GraphQLOperationType = .query
  var operationDefinition: String = ""
  var operationIdentifier: String? = nil
  var operationName: String = "AllCarsQuery"
  var queryDocument: String = ""
  var variables: GraphQLMap? = [:]
}

class AllTrainsQuery: GraphQLOperation {
  typealias Data = MockSelectionSet

  init(id: String) {
  }

  var operationType: GraphQLOperationType = .query
  var operationDefinition: String = ""
  var operationIdentifier: String? = nil
  var operationName: String = "AllTrainsQuery"
  var queryDocument: String = ""
  var variables: GraphQLMap? = [:]
}

class MockSelectionSet: GraphQLSelectionSet {
  static var selections: [GraphQLSelection] = []

  var resultMap: ResultMap
  required init(unsafeResultMap: ResultMap) {
    resultMap = unsafeResultMap
  }
}

This would allow you to still call DataRequest.cars(for: "xyz"), which would return a `DataRequest<AllCarsQuery.Data>.

You should be able to modify this code to convert from the AllCarsQuery.Data to your own view models, so you have it return DataRequest<CarsResults> or whatever you’d like.

1 Like

I’ll check if this approach can solve my problem.

(however I still need to use a generic type (my Returnable) and not a specific struct instance)
as you say I still need to convert the data type. You are right.
However If I can access my returnType, I can switch on the class type and use the correct mapper. (that’s how I make a model factory at the moment)

I am using a enum because adding a new request is simplified by the compiler (providing me what is missing) and following the current standard with the REST request, making it simple to work in the same place

@AnthonyMDev The problem with the above approach is that I am still not able to get a Protocol where I can, on need access the Return Type…

but without the need to specify the Generic Parameter of the protocol.

I need to have DataRequest and not DataRequest<...> because I don’t know the <...> at the intermediate caller site

@AnthonyMDev this is basically what I would like to do:

func make(operation: AnyGraphQLOperation,
              completion: @escaping ((Result<Any, Error>) -> Void)) {
        
        switch query.operationType {
            case .query:
                apollo.fetch(query: query.operation)
                
            case .mutation:
                break
                
            case .subscription:
                break
        }
    }

which requires having a way to get a non-generic AnyGraphQLOperation

which is not possible even with this solution


    let anyOperation: AnyGraphQLOperation = AnyGraphQLOperation<AllCommentsQuery>(base: AllCommentsQuery(collectionId: "123"))
    make(operation: anyOperation)

and

class AnyGraphQLOperation<Operation: GraphQLOperation>: GraphQLOperation {
    typealias Data = Operation.Data

    var operation: Operation<Data>
    
    init<Operation: GraphQLOperation>(base: Operation) where Operation.Data == Data {
        operation = base
    }

    var operationType: GraphQLOperationType
    ....
}

Currently as said in the github repo, there is no way to NOT use a Generated Class directly at call site.
Do you have any example on how to call your SDK without using ApolloClient.fetch directly with a real instance of operation?

I still think that fully erasing the type is here not a best practice. You’ll end up with some untyped thing that you have to start casting to types to use it. It’s unsafe, requires implicit but tightly coupled knowledge, and makes your code less performant (loss of static compiler optimizations on generic types).

Does it work if you do this?

func make<T: Returnable>(operation: AnyGraphQLOperation<T>,
              completion: @escaping ((Result<T.ReturnType, Error>) -> Void))

I think you should be able to make that function work for your purposes.

@AnthonyMDev Thanks for the additional input!
I will try to use that.

I still think that won’t work for me because even if using a AnyGraphQLOperation, I am not able to construct a generic one.

There is no way for me to build a mapper from a generic enum (for example) to return me some AnyGraphQLOperation. The compiler needs to know the underlying type.

I agree with you that this solution requires a manual mapping of the types…
but it’s also the way to create a interface between the app logic and the network logic in 2 completely isolated modules.

The Network for me needs to do some magic mapping, but the app does not need to worry about how the requests are done.

So I am missing the generic request type here

I’ve been on vacation. I hope you found a solution that suits you!

This isn’t a problem with Apollo though. What you’re asking for is not possible with the language. You are literally describing associated types on protocols. And that means they are generic and can’t be referred to without you specifying the generic type. Generics must be determined at compile time, not runtime.

I understand what you want to do, but it’s not possible.

Hi, No I have not yet found a working solution to use the SDK.
I am currently investigating writing my own structs for requests so that I can make them Returnable, which fits my need :frowning: