Skip to content

Commit

Permalink
Building SchemaConfiguration to handle cacheKey resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
AnthonyMDev committed Sep 21, 2021
1 parent e5fa2ca commit 3ad6de9
Show file tree
Hide file tree
Showing 15 changed files with 73 additions and 104 deletions.
8 changes: 4 additions & 4 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; };
DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; };
DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; };
DE2FCF1D26E806710057EA67 /* SchemaTypeMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1C26E806710057EA67 /* SchemaTypeMapper.swift */; };
DE2FCF1D26E806710057EA67 /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1C26E806710057EA67 /* SchemaConfiguration.swift */; };
DE2FCF1F26E807CC0057EA67 /* CacheTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */; };
DE2FCF2126E807EF0057EA67 /* Cacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2026E807EF0057EA67 /* Cacheable.swift */; };
DE2FCF2726E8083A0057EA67 /* Union.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2326E8083A0057EA67 /* Union.swift */; };
Expand Down Expand Up @@ -806,7 +806,7 @@
DE181A3126C5C401000C0B9C /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compression.swift; sourceTree = "<group>"; };
DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = "<group>"; };
DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketStream.swift; sourceTree = "<group>"; };
DE2FCF1C26E806710057EA67 /* SchemaTypeMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTypeMapper.swift; sourceTree = "<group>"; };
DE2FCF1C26E806710057EA67 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaConfiguration.swift; sourceTree = "<group>"; };
DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTransaction.swift; sourceTree = "<group>"; };
DE2FCF2026E807EF0057EA67 /* Cacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cacheable.swift; sourceTree = "<group>"; };
DE2FCF2326E8083A0057EA67 /* Union.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Union.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1799,7 +1799,7 @@
DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */,
DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */,
DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */,
DE2FCF1C26E806710057EA67 /* SchemaTypeMapper.swift */,
DE2FCF1C26E806710057EA67 /* SchemaConfiguration.swift */,
DE664ED326602AF60054DB4F /* Selection.swift */,
DE2FCF2026E807EF0057EA67 /* Cacheable.swift */,
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
Expand Down Expand Up @@ -2923,7 +2923,7 @@
DE05860A266978A100265760 /* ResponseDict.swift in Sources */,
DE05860B266978A100265760 /* SelectionSet.swift in Sources */,
DE9C04AC26EAAE4400EC35E7 /* JSON.swift in Sources */,
DE2FCF1D26E806710057EA67 /* SchemaTypeMapper.swift in Sources */,
DE2FCF1D26E806710057EA67 /* SchemaConfiguration.swift in Sources */,
DE2FCF4526E80CF10057EA67 /* GraphQLOperation.swift in Sources */,
DE2FCF2C26E808560057EA67 /* ObjectType.swift in Sources */,
DE05860C266978A100265760 /* FragmentProtocols.swift in Sources */,
Expand Down
13 changes: 2 additions & 11 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public class ApolloClient {
/// Creates a client with a `RequestChainNetworkTransport` connecting to the specified URL.
///
/// - Parameter url: The URL of a GraphQL server to connect to.
public convenience init(url: URL) {
let store = ApolloStore(cache: InMemoryNormalizedCache())
public convenience init(url: URL, schema: SchemaConfiguration.Type) {
let store = ApolloStore(cache: InMemoryNormalizedCache(), schema: schema)
let provider = DefaultInterceptorProvider(store: store)
let transport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url)
Expand All @@ -73,15 +73,6 @@ public class ApolloClient {

extension ApolloClient: ApolloClientProtocol {

public var cacheKeyForObject: CacheKeyForObject? {
get {
return self.store.cacheKeyForObject
}
set {
self.store.cacheKeyForObject = newValue
}
}

public func clearCache(callbackQueue: DispatchQueue = .main,
completion: ((Result<Void, Error>) -> Void)? = nil) {
self.store.clearCache(callbackQueue: callbackQueue, completion: completion)
Expand Down
3 changes: 0 additions & 3 deletions Sources/Apollo/ApolloClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ public protocol ApolloClientProtocol: AnyObject {
/// A store used as a local cache.
var store: ApolloStore { get }

/// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used.
var cacheKeyForObject: CacheKeyForObject? { get set }

/// Clears the underlying cache.
/// Be aware: In more complex setups, the same underlying cache can be used across multiple instances, so if you call this on one instance, it'll clear that cache across all instances which share that cache.
///
Expand Down
14 changes: 7 additions & 7 deletions Sources/Apollo/ApolloStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protocol ApolloStoreSubscriber: AnyObject {

/// The `ApolloStore` class acts as a local cache for normalized GraphQL results.
public final class ApolloStore {
let config: CacheConfiguration
let schema: SchemaConfiguration.Type

private let cache: NormalizedCache
private let queue: DispatchQueue
Expand All @@ -43,11 +43,11 @@ public final class ApolloStore {
/// - Parameters:
/// - cache: An instance of `normalizedCache` to use to cache results.
/// Defaults to an `InMemoryNormalizedCache`.
/// - configuration: A cache configuration used to resolve objects from the normalized cache.
/// - schema: A schema configuration used to resolve objects from the normalized cache.
public init(cache: NormalizedCache = InMemoryNormalizedCache(),
configuration: CacheConfiguration) {
schema: SchemaConfiguration.Type) {
self.cache = cache
self.config = configuration
self.schema = schema
self.queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent)
}

Expand Down Expand Up @@ -179,13 +179,13 @@ public final class ApolloStore {

public class ReadTransaction {
fileprivate let cache: NormalizedCache
fileprivate let config: CacheConfiguration
fileprivate let schema: SchemaConfiguration.Type

fileprivate lazy var loader: DataLoader<CacheKey, Record> = DataLoader(self.cache.loadRecords)

fileprivate init(store: ApolloStore) {
self.cache = store.cache
self.config = store.config
self.schema = store.schema
}

public func read<Query: GraphQLQuery>(query: Query) throws -> Query.Data {
Expand Down Expand Up @@ -214,7 +214,7 @@ public final class ApolloStore {
) throws -> Accumulator.FinalResult {
let object = try loadObject(forKey: key).get()

let executor = GraphQLExecutor(cacheConfiguration: config) { object, info in
let executor = GraphQLExecutor(schema: schema) { object, info in
return object[info.cacheKeyForField]
} resolveReference: { reference in
self.loadObject(forKey: reference.key)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/CacheWriteInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public struct CacheWriteInterceptor: ApolloInterceptor {
}

do {
let (_, records) = try legacyResponse.parseResult(cacheConfiguration: store.config)
let (_, records) = try legacyResponse.parseResult(schema: store.schema)

guard chain.isNotCancelled else {
return
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/DefaultInterceptorProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ open class DefaultInterceptorProvider: InterceptorProvider {
CacheReadInterceptor(store: self.store),
NetworkFetchInterceptor(client: self.client),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(cacheConfiguration: store.config),
JSONResponseParsingInterceptor(schema: store.schema),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store),
]
Expand Down
59 changes: 5 additions & 54 deletions Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,70 +138,21 @@ public struct GraphQLResultError: Error, LocalizedError {
/// The methods in this class closely follow the
/// [execution algorithm described in the GraphQL specification]
/// (http://spec.graphql.org/draft/#sec-Execution)
/// with an important difference: execution returns a value for every selection in a selection set,
/// not the merged fields. This means we get a separate result for every fragment, even though all
/// fields that share a response key are still executed at the same time for efficiency.
///
/// So given the following query:
///
/// ```
/// query HeroAndFriendsNames {
/// hero {
/// name
/// friends {
/// name
/// }
/// ...FriendsAppearsIn
/// }
/// }
///
/// fragment FriendsAppearsIn on Character {
/// friends {
/// appearsIn
/// }
/// }
/// ```
///
/// A server would return a response with `name` and `appearsIn` merged into one object:
///
/// ```
/// ...
/// {
/// "name": "R2-D2",
/// "friends": [
/// {
/// "name": "Luke Skywalker",
/// "appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"]
/// }
/// }
/// ...
/// ```
///
/// The executor on the other hand, will return a separate value for every selection:
///
/// - `String`
/// - `[HeroAndFriendsNames.Data.Hero.Friend]`
/// - `FriendsAppearsIn`
/// - `[FriendsAppearsIn.Friend]`
///
/// These values then get passed into a generated `GraphQLMappable` initializer, and this is how
/// type safe results get built up.
///
final class GraphQLExecutor {
private let fieldResolver: GraphQLFieldResolver
private let resolveReference: ReferenceResolver?
private let cacheConfiguration: CacheConfiguration
private let schema: SchemaConfiguration.Type

var shouldComputeCachePath = true

/// Creates a GraphQLExecutor that resolves field values by calling the provided resolver. If provided, it will also resolve references by calling
/// the reference resolver.
init(
cacheConfiguration: CacheConfiguration,
schema: SchemaConfiguration.Type,
resolver: @escaping GraphQLFieldResolver,
resolveReference: ReferenceResolver? = nil
) {
self.cacheConfiguration = cacheConfiguration
self.schema = schema
self.fieldResolver = resolver
self.resolveReference = resolveReference
}
Expand All @@ -211,7 +162,7 @@ final class GraphQLExecutor {
}

private func cacheKey(for object: JSONObject) -> String? {
cacheConfiguration.cacheKeyResolver?.cacheKey(for: object)?.key
schema.cacheKey(for: object)?.key
// TODO: Delete - Document in migration log that CacheKey cannot be an array anymore
// if let array = value as? [Any?] {
// return array.compactMap { String(describing: $0) }.joined(separator: "_")
Expand Down Expand Up @@ -285,7 +236,7 @@ final class GraphQLExecutor {
guard let __typename = json["__typename"] as? String else {
return nil
}
return cacheConfiguration.schemaTypeMapper.objectType(forTypename: __typename)
return schema.objectType(forTypename: __typename)
}

private func groupFields(
Expand Down
6 changes: 4 additions & 2 deletions Sources/Apollo/GraphQLResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public final class GraphQLResponse<Data: SelectionSet> {
/// The `RecordSet` can be merged into a local cache.
/// - Parameter cacheConfiguration: See `CacheConfiguration`
/// - Returns: A `GraphQLResult` and a `RecordSet`.
public func parseResult(cacheConfiguration: CacheConfiguration) throws -> (GraphQLResult<Data>, RecordSet?) {
public func parseResult(
schema: SchemaConfiguration.Type
) throws -> (GraphQLResult<Data>, RecordSet?) {
let errors: [GraphQLError]?

if let errorsEntry = body["errors"] as? [JSONObject] {
Expand All @@ -33,7 +35,7 @@ public final class GraphQLResponse<Data: SelectionSet> {
let extensions = body["extensions"] as? JSONObject

if let dataEntry = body["data"] as? JSONObject {
let executor = GraphQLExecutor(cacheConfiguration: cacheConfiguration) { object, info in
let executor = GraphQLExecutor(schema: schema) { object, info in
return object[info.responseKeyForField]
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/Apollo/JSONResponseParsingInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ public struct JSONResponseParsingInterceptor: ApolloInterceptor {
}
}

public let cacheConfiguration: CacheConfiguration
public let schema: SchemaConfiguration.Type

/// Designated Initializer
public init(cacheConfiguration: CacheConfiguration) {
self.cacheConfiguration = cacheConfiguration
public init(schema: SchemaConfiguration.Type) {
self.schema = schema
}

public func interceptAsync<Operation: GraphQLOperation>(
Expand Down Expand Up @@ -82,7 +82,7 @@ public struct JSONResponseParsingInterceptor: ApolloInterceptor {
// There is no cache, so we don't need to get any info on dependencies. Use fast parsing.
return try response.parseResultFast()
default:
let (parsedResult, _) = try response.parseResult(cacheConfiguration: cacheConfiguration)
let (parsedResult, _) = try response.parseResult(schema: schema)
return parsedResult
}
}
Expand Down
16 changes: 5 additions & 11 deletions Sources/ApolloAPI/CodegenV1/CacheTransaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,29 @@ public protocol CacheKeyResolver {
func cacheKey(for: [String: Any]) -> CacheKey?
}

// TODO: Document
public struct CacheConfiguration {
public let schemaTypeMapper: SchemaTypeMapper.Type
public let cacheKeyResolver: CacheKeyResolver?
}

public class CacheTransaction {
let config: CacheConfiguration
let schema: SchemaConfiguration.Type
private(set) var errors: [CacheError] = []
private var fetchedObjects: [CacheKey: Object] = [:]

init(config: CacheConfiguration) {
self.config = config
init(schema: SchemaConfiguration.Type) {
self.schema = schema
}

func object(withKey key: CacheKey) -> Object? {
fetchedObjects[key] // TODO: if not fetched yet, fetch from store
}

func object(withData data: [String: Any]) -> Object {
let cacheKey = config.cacheKeyResolver?.cacheKey(for: data)
let cacheKey = schema.cacheKey(for: data)

if let cacheKey = cacheKey, let object = fetchedObjects[cacheKey] {
return object
// TODO: should merge data objects if needed?
}

guard let typename = data["__typename"] as? String,
let type = config.schemaTypeMapper.objectType(forTypename: typename) else {
let type = schema.objectType(forTypename: typename) else {
fatalError()
}

Expand Down
38 changes: 38 additions & 0 deletions Sources/ApolloAPI/CodegenV1/SchemaConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
public protocol SchemaConfiguration {
static func objectType(forTypename __typename: String) -> Object.Type?
}

extension SchemaConfiguration {
public static func cacheKey(for data: [String: JSONValue]) -> CacheKey? {
guard let __typename = data["__typename"] as? String,
let keyString = cacheKeyString(for: data, withTypename: __typename) else {
return nil
}
return CacheKey(__typename + ":" + keyString)
}

private static func cacheKeyString(
for data: [String: JSONValue],
withTypename __typename: String
) -> String? {
if let objectType = objectType(forTypename: __typename),
let resolver = objectType.Metadata as? ObjectCacheKeyResolver.Type {
return resolver.cacheKey(for: data)
}

if let unknownTypeMapper = self as? SchemaUnknownTypeMapper.Type {
return unknownTypeMapper.cacheKeyForUnknownType(withTypename: __typename, data: data)
}

return nil
}
}

public protocol ObjectCacheKeyResolver {
static func cacheKey(for: [String: JSONValue]) -> String?
}

public protocol SchemaUnknownTypeMapper {
static func cacheKeyForUnknownType(withTypename: String, data: [String: JSONValue]) -> String?
}

3 changes: 0 additions & 3 deletions Sources/ApolloAPI/CodegenV1/SchemaTypeMapper.swift

This file was deleted.

1 change: 0 additions & 1 deletion Sources/ApolloAPI/CodegenV1/SchemaTypes/Object.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ extension Object {
func implements(_ interface: Interface.Type) -> Bool {
implementedInterfaces?.contains(where: { $0 == interface }) ?? false
}

}

public static func _canBeConverted(to otherType: ParentType) -> Bool {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ApolloAPI/CodegenV1/SelectionSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public enum ParentType {

public protocol SelectionSet: AnySelectionSet {

associatedtype Schema: SchemaTypeMapper
associatedtype Schema: SchemaConfiguration
}

extension SelectionSet {
Expand Down
2 changes: 1 addition & 1 deletion Sources/StarWarsAPI/APIv1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Foundation
public protocol SelectionSet: ApolloAPI.SelectionSet
where Schema == StarWarsAPITypeFactory {}

public enum StarWarsAPITypeFactory: SchemaTypeMapper {
public enum StarWarsAPITypeFactory: SchemaConfiguration {
public static func objectType(forTypename __typename: String) -> Object.Type? {
switch __typename {
case "Bird": return Bird.self
Expand Down

0 comments on commit 3ad6de9

Please sign in to comment.