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