Skip to content

Commit 79f47bf

Browse files
gh-action-runnergh-action-runner
authored andcommitted
Squashed 'apollo-ios/' changes from 9e4bad9cc..c5a0eef72
c5a0eef72 feature: @fieldPolicy directive (#735) git-subtree-dir: apollo-ios git-subtree-split: c5a0eef72972927864af6bda056095b0ba615dd0
1 parent 880fd7e commit 79f47bf

File tree

6 files changed

+347
-8
lines changed

6 files changed

+347
-8
lines changed

Sources/Apollo/ExecutionSources/CacheDataExecutionSource.swift

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
3434
with info: FieldExecutionInfo,
3535
on object: Record
3636
) -> PossiblyDeferred<AnyHashable?> {
37-
PossiblyDeferred {
38-
let value = try object[info.cacheKeyForField()]
37+
PossiblyDeferred {
38+
39+
let value = try resolveCacheKey(with: info, on: object)
3940

4041
switch value {
4142
case let reference as CacheReference:
@@ -67,6 +68,73 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
6768
}
6869
}
6970
}
71+
72+
private func resolveCacheKey(
73+
with info: FieldExecutionInfo,
74+
on object: Record
75+
) throws -> AnyHashable? {
76+
if let fieldPolicyResult = resolveProgrammaticFieldPolicy(with: info, and: info.field.type) ??
77+
FieldPolicyDirectiveEvaluator(field: info.field, variables: info.parentInfo.variables)?.resolveFieldPolicy(),
78+
let returnTypename = typename(for: info.field) {
79+
80+
switch fieldPolicyResult {
81+
case .single(let key):
82+
return object[formatCacheKey(withInfo: key, andTypename: returnTypename)]
83+
case .list(let keys):
84+
return keys.map { object[formatCacheKey(withInfo: $0, andTypename: returnTypename)] }
85+
}
86+
}
87+
88+
let key = try info.cacheKeyForField()
89+
return object[key]
90+
}
91+
92+
private func resolveProgrammaticFieldPolicy(
93+
with info: FieldExecutionInfo,
94+
and type: Selection.Field.OutputType
95+
) -> FieldPolicyResult? {
96+
guard let provider = info.parentInfo.schema.configuration.self as? (any FieldPolicyProvider.Type) else {
97+
return nil
98+
}
99+
100+
switch type {
101+
case .nonNull(let innerType):
102+
return resolveProgrammaticFieldPolicy(with: info, and: innerType)
103+
case .list(_):
104+
if let keys = provider.cacheKeyList(
105+
for: info.field,
106+
variables: info.parentInfo.variables,
107+
path: info.responsePath
108+
) {
109+
return .list(keys)
110+
}
111+
default:
112+
if let key = provider.cacheKey(
113+
for: info.field,
114+
variables: info.parentInfo.variables,
115+
path: info.responsePath
116+
) {
117+
return .single(key)
118+
}
119+
}
120+
return nil
121+
}
122+
123+
private func formatCacheKey(
124+
withInfo info: CacheKeyInfo,
125+
andTypename typename: String
126+
) -> String {
127+
return "\(info.uniqueKeyGroup ?? typename):\(info.id)"
128+
}
129+
130+
private func typename(for field: Selection.Field) -> String? {
131+
switch field.type.namedType {
132+
case .object(let selectionSetType):
133+
return selectionSetType.__parentType.__typename
134+
default:
135+
return nil
136+
}
137+
}
70138

71139
private func deferredResolve(reference: CacheReference) -> PossiblyDeferred<Record> {
72140
guard let transaction else {
@@ -102,5 +170,3 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
102170
}
103171
}
104172
}
105-
106-
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import Foundation
2+
#if !COCOAPODS
3+
import ApolloAPI
4+
#endif
5+
6+
enum FieldPolicyResult {
7+
case single(CacheKeyInfo)
8+
case list([CacheKeyInfo])
9+
}
10+
11+
struct FieldPolicyDirectiveEvaluator {
12+
let field: Selection.Field
13+
let fieldPolicy: Selection.FieldPolicy
14+
let arguments: [String: InputValue]
15+
let variables: GraphQLOperation.Variables?
16+
17+
init?(
18+
field: Selection.Field,
19+
variables: GraphQLOperation.Variables?
20+
) {
21+
self.field = field
22+
self.variables = variables
23+
24+
guard let fieldPolicy = field.fieldPolicy else {
25+
return nil
26+
}
27+
self.fieldPolicy = fieldPolicy
28+
29+
guard let arguments = field.arguments else {
30+
return nil
31+
}
32+
self.arguments = arguments
33+
}
34+
35+
func resolveFieldPolicy() -> FieldPolicyResult? {
36+
let keyArgs = parseKeyArgs(for: fieldPolicy)
37+
38+
var singleValueArgs = [String?](repeating: nil, count: keyArgs.count)
39+
var listArgIndex: Int? = nil
40+
var listArgValues: [String] = []
41+
42+
for (index, arg) in keyArgs.enumerated() {
43+
guard let argVal = arguments[arg.name] else {
44+
return nil
45+
}
46+
47+
guard let resolved = argVal.resolveValue(
48+
keyPath: arg.path,
49+
variables: variables
50+
), !resolved.isEmpty else {
51+
return nil
52+
}
53+
54+
if resolved.count > 1 {
55+
listArgIndex = index
56+
listArgValues = resolved
57+
} else {
58+
guard let value = resolved.first else {
59+
return nil
60+
}
61+
singleValueArgs[index] = value
62+
}
63+
}
64+
65+
if let listArgIndex = listArgIndex {
66+
guard let cacheKeyList = processKeyList(
67+
listArgIndex: listArgIndex,
68+
listArgValues: listArgValues,
69+
singleValueArgs: singleValueArgs,
70+
keyArgs: keyArgs
71+
) else {
72+
return nil
73+
}
74+
return .list(cacheKeyList)
75+
} else {
76+
let parts = singleValueArgs.compactMap { $0 }
77+
return .single(CacheKeyInfo(id: parts.joined(separator: "+")))
78+
}
79+
}
80+
81+
private func processKeyList(
82+
listArgIndex: Int,
83+
listArgValues: [String],
84+
singleValueArgs: [String?],
85+
keyArgs: [ParsedKey]
86+
) -> [CacheKeyInfo]? {
87+
var keys: [CacheKeyInfo] = []
88+
keys.reserveCapacity(listArgValues.count)
89+
for value in listArgValues {
90+
var parts = [String]()
91+
parts.reserveCapacity(keyArgs.count)
92+
for keyIndex in 0..<keyArgs.count {
93+
if keyIndex == listArgIndex {
94+
parts.append(value)
95+
} else if let singleValue = singleValueArgs[keyIndex] {
96+
parts.append(singleValue)
97+
} else {
98+
return nil
99+
}
100+
}
101+
keys.append(CacheKeyInfo(id: parts.joined(separator: "+")))
102+
}
103+
return keys
104+
}
105+
106+
private func parseKeyArgs(for fieldPolicy: Selection.FieldPolicy) -> [ParsedKey] {
107+
fieldPolicy.keyArgs.map { key in
108+
if let dot = key.firstIndex(of: ".") {
109+
let name = String(key[..<dot])
110+
let rest = key[key.index(after: dot)...]
111+
return ParsedKey(name: name, path: rest.split(separator: ".").map(String.init))
112+
} else {
113+
return ParsedKey(name: key, path: nil)
114+
}
115+
}
116+
}
117+
118+
private struct ParsedKey {
119+
let name: String
120+
let path: [String]?
121+
}
122+
}
123+
124+
extension ScalarType {
125+
fileprivate var cacheKeyComponentStringValue: String {
126+
switch self {
127+
case let strVal as String:
128+
return strVal
129+
130+
case let boolVal as Bool:
131+
return boolVal ? "true" : "false"
132+
133+
case let intVal as Int:
134+
return String(intVal)
135+
136+
case let doubleVal as Double:
137+
return String(doubleVal)
138+
139+
case let floatVal as Float:
140+
return String(floatVal)
141+
142+
default:
143+
return String(describing: self)
144+
}
145+
}
146+
}
147+
148+
extension JSONValue {
149+
fileprivate func cacheKeyComponentStringValue(keyPath: [String]? = nil) -> [String]? {
150+
switch self {
151+
case let strVal as String:
152+
return [strVal]
153+
154+
case let boolVal as Bool:
155+
return boolVal ? ["true"] : ["false"]
156+
157+
case let intVal as Int:
158+
return [String(intVal)]
159+
160+
case let doubleVal as Double:
161+
return [String(doubleVal)]
162+
163+
case let floatVal as Float:
164+
return [String(floatVal)]
165+
166+
case let arrVal as [JSONValue]:
167+
let values: [String] = arrVal.compactMap { $0.cacheKeyComponentStringValue()?.first }
168+
guard !values.isEmpty else { return nil }
169+
return values
170+
171+
case let objVal as JSONObject:
172+
guard let keyPath, !keyPath.isEmpty else { return nil }
173+
guard let targetValue = objVal.traverse(to: keyPath[...]) else { return nil }
174+
return targetValue.cacheKeyComponentStringValue()
175+
176+
default:
177+
return [String(describing: self)]
178+
}
179+
}
180+
}
181+
182+
extension JSONObject {
183+
fileprivate func traverse(
184+
to path: ArraySlice<String>
185+
) -> JSONValue? {
186+
guard let head = path.first else { return self }
187+
guard let next = self[head] else { return nil }
188+
if path.count == 1 { return next }
189+
if let nested = next as? JSONObject {
190+
return nested.traverse(to: path.dropFirst())
191+
}
192+
return nil
193+
}
194+
}
195+
196+
extension InputValue {
197+
fileprivate func resolveValue(keyPath: [String]? = nil, variables: [String: (any GraphQLOperationVariableValue)]? = nil) -> [String]? {
198+
switch self {
199+
case .scalar(let scalar):
200+
return [scalar.cacheKeyComponentStringValue]
201+
case .variable(let varName):
202+
guard let varValue = variables?[varName] else {
203+
return nil
204+
}
205+
return varValue._jsonEncodableValue?._jsonValue.cacheKeyComponentStringValue(keyPath: keyPath)
206+
case .list(let list):
207+
if list.contains(where: { if case .list = $0 { return true } else { return false } }) {
208+
return nil
209+
}
210+
let values = list.compactMap { $0.resolveValue()?.first }
211+
guard !values.isEmpty else { return nil }
212+
return values
213+
case .object(let dict):
214+
guard let keyPath, !keyPath.isEmpty else { return nil }
215+
guard let targetValue = self.traverse(through: dict, to: keyPath[...]) else { return nil }
216+
return targetValue.resolveValue()
217+
default:
218+
return nil
219+
}
220+
}
221+
222+
fileprivate func traverse(
223+
through dict: [String: InputValue],
224+
to path: ArraySlice<String>
225+
) -> InputValue? {
226+
guard let head = path.first else { return .object(dict) }
227+
guard let next = dict[head] else { return nil }
228+
if path.count == 1 { return next }
229+
if case .object(let nested) = next {
230+
return traverse(through: nested, to: path.dropFirst())
231+
}
232+
return nil
233+
}
234+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// A protocol that can be added to the ``SchemaConfiguration`` in order to provide custom field policy configuration.
2+
///
3+
/// This protocol should be applied to your existing ``SchemaConfiguration`` and provides a way to provide custom
4+
/// field policy cache keys in lieu of using the @fieldPolicy directive.
5+
public protocol FieldPolicyProvider {
6+
/// The entry point for resolving a cache key to read an object from the `NormalizedCache` for a field
7+
/// that returns a single object.
8+
///
9+
/// - Parameters:
10+
/// - field: The ``Selection.Field`` of the operation being executed.
11+
/// - variables: Optional ``GraphQLOperation.Variables`` input values provided to the operation.
12+
/// - path: The ``ResponsePath`` representing the path within operation to get to the given field.
13+
/// - Returns: A ``CacheKeyInfo`` describing the computed cache key.
14+
static func cacheKey(for field: Selection.Field, variables: GraphQLOperation.Variables?, path: ResponsePath) -> CacheKeyInfo?
15+
16+
/// The entry point for resolving cache keys to read objects from the `NormalizedCache` for a field
17+
/// that returns a list of objects.
18+
///
19+
/// - Parameters:
20+
/// - field: The ``Selection.Field`` of the operation being executed.
21+
/// - variables: Optional ``GraphQLOperation.Variables`` input values provided to the operation.
22+
/// - path: The ``ResponsePath`` representing the path within operation to get to the given field.
23+
/// - Returns: An array of ``CacheKeyInfo`` describing the computed cache keys.
24+
static func cacheKeyList(for listField: Selection.Field, variables: GraphQLOperation.Variables?, path: ResponsePath) -> [CacheKeyInfo]?
25+
}
File renamed without changes.

Sources/ApolloAPI/SchemaConfiguration.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,4 @@ public protocol SchemaConfiguration {
3939
/// Represented as a ``ObjectData`` dictionary.
4040
/// - Returns: A ``CacheKeyInfo`` describing the computed cache key for the response object.
4141
static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo?
42-
4342
}

0 commit comments

Comments
 (0)