Skip to content

Commit f38dd44

Browse files
x-sheepgh-action-runner
authored and
gh-action-runner
committed
feat: Specify caching fields with typePolicy directive (apollographql/apollo-ios-dev#554)
1 parent 9ec4b0b commit f38dd44

File tree

6 files changed

+96
-17
lines changed

6 files changed

+96
-17
lines changed

Sources/Apollo/ExecutionSources/CacheDataExecutionSource.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
7272
return transaction.loadObject(forKey: reference.key)
7373
}
7474

75-
func computeCacheKey(for object: Record, in schema: any SchemaMetadata.Type) -> CacheKey? {
75+
func computeCacheKey(
76+
for object: Record,
77+
in schema: any SchemaMetadata.Type,
78+
inferredToImplementInterface interface: Interface?
79+
) -> CacheKey? {
7680
return object.key
7781
}
7882

Sources/Apollo/GraphQLExecutionSource.swift

+15-3
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,18 @@ public protocol GraphQLExecutionSource {
4040
/// - Parameters:
4141
/// - object: The data for the object from the source.
4242
/// - schema: The schema that the type the object data represents belongs to.
43+
/// - implementedInterface: An optional ``Interface`` that the object is
44+
/// inferred to implement. If the cache key is being resolved for a selection set with an
45+
/// interface as it's `__parentType`, you can infer the object must implement that interface.
46+
/// You should provide that interface to this parameter.
4347
/// - Returns: A cache key for normalizing the object in the cache. If `nil` is returned the
4448
/// object is assumed to be stored in the cache with no normalization. The executor will
4549
/// construct a cache key based on the object's path in its enclosing operation.
46-
func computeCacheKey(for object: RawObjectData, in schema: any SchemaMetadata.Type) -> CacheKey?
50+
func computeCacheKey(
51+
for object: RawObjectData,
52+
in schema: any SchemaMetadata.Type,
53+
inferredToImplementInterface implementedInterface: Interface?
54+
) -> CacheKey?
4755
}
4856

4957
/// A type of `GraphQLExecutionSource` that uses the user defined cache key computation
@@ -57,8 +65,12 @@ public protocol CacheKeyComputingExecutionSource: GraphQLExecutionSource {
5765
}
5866

5967
extension CacheKeyComputingExecutionSource {
60-
@_spi(Execution) public func computeCacheKey(for object: RawObjectData, in schema: any SchemaMetadata.Type) -> CacheKey? {
68+
@_spi(Execution) public func computeCacheKey(
69+
for object: RawObjectData,
70+
in schema: any SchemaMetadata.Type,
71+
inferredToImplementInterface implementedInterface: Interface?
72+
) -> CacheKey? {
6173
let dataWrapper = opaqueObjectDataWrapper(for: object)
62-
return schema.cacheKey(for: dataWrapper)
74+
return schema.cacheKey(for: dataWrapper, inferredToImplementInterface: implementedInterface)
6375
}
6476
}

Sources/Apollo/GraphQLExecutor.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -493,9 +493,15 @@ public final class GraphQLExecutor<Source: GraphQLExecutionSource> {
493493
onChildObject object: Source.RawObjectData,
494494
accumulator: Accumulator
495495
) -> PossiblyDeferred<Accumulator.PartialResult> {
496+
let expectedInterface = rootSelectionSetType.__parentType as? Interface
497+
496498
let (childExecutionInfo, selections) = fieldInfo.computeChildExecutionData(
497499
withRootType: rootSelectionSetType,
498-
cacheKey: executionSource.computeCacheKey(for: object, in: fieldInfo.parentInfo.schema)
500+
cacheKey: executionSource.computeCacheKey(
501+
for: object,
502+
in: fieldInfo.parentInfo.schema,
503+
inferredToImplementInterface: expectedInterface
504+
)
499505
)
500506

501507
return execute(

Sources/ApolloAPI/SchemaMetadata.swift

+45-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
/// A protocol that a generated GraphQL schema should conform to.
24
///
35
/// The generated schema metadata is the source of information about the generated types in the
@@ -43,20 +45,53 @@ extension SchemaMetadata {
4345
/// Resolves the cache key for an object in a GraphQL response to be used by
4446
/// `NormalizedCache` mechanisms.
4547
///
46-
/// Maps the type of the `object` using the ``graphQLType(for:)`` function, then gets the
47-
/// ``CacheKeyInfo`` for the `object` using the ``SchemaConfiguration/cacheKeyInfo(for:object:)``
48-
/// function.
49-
/// Finally, this function transforms the ``CacheKeyInfo`` into the correct ``CacheReference``
50-
/// for the `NormalizedCache`.
48+
/// The algorithm for resolving the objects cache key:
49+
/// 1. Map the type of the `object` using the ``graphQLType(for:)`` function.
50+
/// 2. Attempt to gets the `CacheKeyInfo`` using programmatic cache key configuration.
51+
/// 2a. Call the ``SchemaConfiguration/cacheKeyInfo(for:object:)`` function.
52+
/// 2b. If `CacheKeyInfo` is found, transforms the ``CacheKeyInfo`` into the correct ``CacheReference``
53+
/// for the `NormalizedCache` and return it.
54+
/// 3. If no programmatic cache key is returned, attempt to resolve the `keyFields` for the object
55+
/// 3a. Check if the object's type has `keyFields`.
56+
/// 3b. If the type of the object is unknown (ie. it cannot be found by ``graphQLType(for:)``),
57+
/// or the type does not have `keyFields`, check if the inferred interface for the type has
58+
/// `keyFields`.
59+
/// 3c. If `keyFields` are found, resolve the cache key by escaping and joining the values of
60+
/// the `keyFields` on the object. Return the resolved cache key.
61+
/// 4. If a cache key is not resolved programmatically or using `keyFields`, return `nil`.
5162
///
5263
/// - Parameter object: A ``JSONObject`` dictionary representing an object in a GraphQL response.
64+
/// - Parameter implementedInterface: An optional ``Interface`` that the object is
65+
/// inferred to implement. If the cache key is being resolved for a selection set with an
66+
/// interface as it's `__parentType`, you can infer the object must implement that interface.
67+
/// You should provide that interface to this parameter.
68+
///
5369
/// - Returns: A `String` representing the cache key for the `object` to be used by
5470
/// `NormalizedCache` mechanisms.
55-
@inlinable public static func cacheKey(for object: ObjectData) -> String? {
56-
guard let type = graphQLType(for: object),
57-
let info = configuration.cacheKeyInfo(for: type, object: object) else {
58-
return nil
71+
@inlinable public static func cacheKey(
72+
for object: ObjectData,
73+
inferredToImplementInterface implementedInterface: Interface? = nil
74+
) -> String? {
75+
guard let type = graphQLType(for: object) else { return nil }
76+
77+
if let info = configuration.cacheKeyInfo(for: type, object: object) {
78+
return "\(info.uniqueKeyGroup ?? type.typename):\(info.id)"
79+
}
80+
81+
guard let keyFields = type.keyFields ?? implementedInterface?.keyFields else { return nil }
82+
83+
let idValues = try? keyFields.map {
84+
guard let keyFieldValue = object[$0] else {
85+
throw JSONDecodingError.missingValue
86+
}
87+
let item = try String(_jsonValue: keyFieldValue._asAnyHashable)
88+
89+
// Escape all instances of `+` with a backslash, as well as other backslashes
90+
return item.replacingOccurrences(of: "\\", with: "\\\\")
91+
.replacingOccurrences(of: "+", with: "\\+")
5992
}
60-
return "\(info.uniqueKeyGroup ?? type.typename):\(info.id)"
93+
94+
guard let id = idValues?.joined(separator: "+") else { return nil }
95+
return "\(type.typename):\(id)"
6196
}
6297
}

Sources/ApolloAPI/SchemaTypes/Interface.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,21 @@
77
public struct Interface: Hashable, Sendable {
88
/// The name of the ``Interface`` in the GraphQL schema.
99
public let name: String
10+
11+
/// A list of fields used to uniquely identify an instance of an object implementing this interface.
12+
///
13+
/// This is set by adding a `@typePolicy` directive to the schema.
14+
public let keyFields: [String]?
1015

1116
/// Designated Initializer
1217
///
1318
/// - Parameter name: The name of the ``Interface`` in the GraphQL schema.
14-
public init(name: String) {
19+
public init(name: String, keyFields: [String]? = nil) {
1520
self.name = name
21+
if keyFields?.isEmpty == false {
22+
self.keyFields = keyFields
23+
} else {
24+
self.keyFields = nil
25+
}
1626
}
1727
}

Sources/ApolloAPI/SchemaTypes/Object.swift

+13-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ public struct Object: Hashable, Sendable {
1010
/// - Parameters:
1111
/// - typename: The name of the type.
1212
/// - implementedInterfaces: A list of the interfaces implemented by the type.
13+
/// - keyFields: A list of field names that are used to uniquely identify an instance of this type.
1314
public init(
1415
typename: String,
15-
implementedInterfaces: [Interface]
16+
implementedInterfaces: [Interface],
17+
keyFields: [String]? = nil
1618
) {
1719
self.typename = typename
1820
self.implementedInterfaces = implementedInterfaces
21+
if keyFields?.isEmpty == false {
22+
self.keyFields = keyFields
23+
} else {
24+
self.keyFields = nil
25+
}
1926
}
2027

2128
/// A list of the interfaces implemented by the type.
@@ -26,6 +33,11 @@ public struct Object: Hashable, Sendable {
2633
/// When an entity of the type is included in a GraphQL response its `__typename` field will
2734
/// match this value.
2835
public let typename: String
36+
37+
/// A list of fields used to uniquely identify an instance of this object.
38+
///
39+
/// This is set by adding a `@typePolicy` directive to the schema.
40+
public let keyFields: [String]?
2941

3042
/// A helper function to determine if the receiver implements a given ``Interface`` Type.
3143
///

0 commit comments

Comments
 (0)