Skip to content

Commit 38098aa

Browse files
BobaFettersAnthonyMDevgh-action-runnergh-action-runnercalvincestari
authored
feature: 2.0 field policy update (#766)
Co-authored-by: Anthony Miller <anthonymdev@gmail.com> Co-authored-by: gh-action-runner <runner@sat12-bq147_e4c437e3-25e8-453a-90e2-c8d5d7208aa2-0AAFF19DA324.local> Co-authored-by: gh-action-runner <runner@Mac-1757445469161.local> Co-authored-by: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Co-authored-by: gh-action-runner <runner@sjc22-bt143-3d3da2ed-9657-4bff-aa86-47c00aab8f5d-DEDB3FF0D288.local> Co-authored-by: Chris Mays <chrsmys@gmail.com> Co-authored-by: gh-action-runner <runner@sjc22-be111-30ac1b9d-ee36-40b7-aa1b-628a929ba3b7-56885318298B.local> Co-authored-by: gh-action-runner <runner@sat12-dp150-eb566756-e3ca-4653-87cc-638153f68524-DEC4847A7769.local> Co-authored-by: gh-action-runner <runner@sat12-bq146-15ddf6c4-760f-4dcd-9438-a10500865d07-CAEB3581E0C4.local>
1 parent 594c0aa commit 38098aa

9 files changed

Lines changed: 321 additions & 59 deletions

File tree

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 🔮 Apollo iOS Roadmap
22

3-
**Last updated: 2025-09-03**
3+
**Last updated: 2025-09-16**
44

55
For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-ios/blob/main/CHANGELOG.md).
66

@@ -30,7 +30,7 @@ Beta Release Milestones:
3030

3131
* ✅ Apollo-iOS
3232
* ✅ ApolloCodegenLib
33-
* 🔲 GraphQLQueryWatcher
33+
* 🔲 Documentation
3434
* 🔲 ApolloWebSocket (Will be released with Apollo-iOS 2.1)
3535

3636
Current RFC for design is available [here](https://github.com/apollographql/apollo-ios/issues/3411)._

Sources/Apollo/Execution/ExecutionSources/CacheDataExecutionSource.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
9797
with info: FieldExecutionInfo,
9898
and type: Selection.Field.OutputType
9999
) -> FieldPolicyResult? {
100-
guard let provider = info.parentInfo.schema.configuration.self as? (any FieldPolicyProvider.Type) else {
100+
guard let provider = info.parentInfo.schema.configuration.self as? (any FieldPolicy.Provider.Type),
101+
let arguments = info.field.arguments else {
101102
return nil
102103
}
103104

@@ -106,16 +107,16 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
106107
return resolveProgrammaticFieldPolicy(with: info, and: innerType)
107108
case .list(_):
108109
if let keys = provider.cacheKeyList(
109-
for: info.field,
110-
variables: info.parentInfo.variables,
110+
for: FieldPolicy.Field(info.field),
111+
inputData: FieldPolicy.InputData(_rawType: .inputValue(arguments), _variables: info.parentInfo.variables),
111112
path: info.responsePath
112113
) {
113114
return .list(keys)
114115
}
115116
default:
116117
if let key = provider.cacheKey(
117-
for: info.field,
118-
variables: info.parentInfo.variables,
118+
for: FieldPolicy.Field(info.field),
119+
inputData: FieldPolicy.InputData(_rawType: .inputValue(arguments), _variables: info.parentInfo.variables),
119120
path: info.responsePath
120121
) {
121122
return .single(key)

Sources/Apollo/FieldPolicyDirectiveEvaluator.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ enum FieldPolicyResult {
1010

1111
struct FieldPolicyDirectiveEvaluator {
1212
let field: Selection.Field
13-
let fieldPolicy: Selection.FieldPolicy
13+
let fieldPolicy: Selection.FieldPolicyDirective
1414
let arguments: [String: InputValue]
1515
let variables: GraphQLOperation.Variables?
1616

@@ -103,7 +103,7 @@ struct FieldPolicyDirectiveEvaluator {
103103
return keys
104104
}
105105

106-
private func parseKeyArgs(for fieldPolicy: Selection.FieldPolicy) -> [ParsedKey] {
106+
private func parseKeyArgs(for fieldPolicy: Selection.FieldPolicyDirective) -> [ParsedKey] {
107107
fieldPolicy.keyArgs.map { key in
108108
if let dot = key.firstIndex(of: ".") {
109109
let name = String(key[..<dot])
@@ -232,4 +232,5 @@ extension InputValue {
232232
return [String(describing: self)]
233233
}
234234
}
235+
235236
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
public enum FieldPolicy {
2+
3+
/// A protocol that can be added to the ``SchemaConfiguration`` in order to provide custom field policy configuration.
4+
///
5+
/// This protocol should be applied to your existing ``SchemaConfiguration`` and provides a way to provide custom
6+
/// field policy cache keys in lieu of using the @fieldPolicy directive.
7+
public protocol Provider {
8+
/// The entry point for resolving a cache key to read an object from the `NormalizedCache` for a field
9+
/// that returns a single object.
10+
///
11+
/// - Parameters:
12+
/// - field: The ``FieldPolicy.Field`` of the operation being executed.
13+
/// - inputData: The ``FieldPolicy.InputData`` representing the arguments and variables for the operation being executed.
14+
/// - path: The ``ResponsePath`` representing the path within operation to get to the given field.
15+
/// - Returns: A ``CacheKeyInfo`` describing the computed cache key.
16+
static func cacheKey(
17+
for field: Field,
18+
inputData: InputData,
19+
path: ResponsePath
20+
) -> CacheKeyInfo?
21+
22+
/// The entry point for resolving cache keys to read objects from the `NormalizedCache` for a field
23+
/// that returns a list of objects.
24+
///
25+
/// - Parameters:
26+
/// - field: The ``FieldPolicy.Field`` of the operation being executed.
27+
/// - inputData: The ``FieldPolicy.InputData`` representing the arguments and variables for the operation being executed.
28+
/// - path: The ``ResponsePath`` representing the path within operation to get to the given field.
29+
/// - Returns: An array of ``CacheKeyInfo`` describing the computed cache keys.
30+
static func cacheKeyList(
31+
for listField: Field,
32+
inputData: InputData,
33+
path: ResponsePath
34+
) -> [CacheKeyInfo]?
35+
}
36+
37+
public struct Field {
38+
public let name: String
39+
public let alias: String?
40+
public let type: Selection.Field.OutputType
41+
42+
public var responseKey: String {
43+
return alias ?? name
44+
}
45+
46+
public init(
47+
name: String,
48+
alias: String?,
49+
type: Selection.Field.OutputType
50+
) {
51+
self.name = name
52+
self.alias = alias
53+
self.type = type
54+
}
55+
56+
public init(_ selectionField: Selection.Field) {
57+
self.name = selectionField.name
58+
self.alias = selectionField.alias
59+
self.type = selectionField.type
60+
}
61+
}
62+
63+
}

Sources/ApolloAPI/FieldPolicyProvider.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

Sources/ApolloAPI/InputData.swift

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import Foundation
2+
3+
extension FieldPolicy {
4+
5+
/// An opaque wrapper for input data of a GraphQL operation. This type wraps data from the
6+
/// arguments and variables for the given field/operation to provide an easy way to work
7+
/// with that data to create cache keys.
8+
public struct InputData {
9+
public let _rawType: RawType
10+
public let _variables: GraphQLOperation.Variables?
11+
12+
public init(
13+
_rawType: RawType,
14+
_variables: GraphQLOperation.Variables?
15+
) {
16+
self._rawType = _rawType
17+
self._variables = _variables
18+
}
19+
20+
@inlinable public subscript(_ key: String) -> (any ScalarType)? {
21+
switch _rawType {
22+
case .inputValue(let dict):
23+
guard let value = dict[key] else {
24+
return nil
25+
}
26+
return value.toAnyScalar(_variables: _variables)
27+
case .json(let dict):
28+
guard let value = dict[key] else {
29+
return nil
30+
}
31+
return jsonValueToAnyScalar(value)
32+
}
33+
}
34+
35+
@_disfavoredOverload
36+
@inlinable public subscript(_ key: String) -> InputListData? {
37+
switch _rawType {
38+
case .inputValue(let dict):
39+
guard let value = dict[key] else {
40+
return nil
41+
}
42+
return value.toFieldInputListData(_variables: _variables)
43+
case .json(let dict):
44+
guard let value = dict[key] else {
45+
return nil
46+
}
47+
return jsonValueToFieldInputListData(value)
48+
}
49+
}
50+
51+
@_disfavoredOverload
52+
@inlinable public subscript(_ key: String) -> InputData? {
53+
switch _rawType {
54+
case .inputValue(let dict):
55+
guard let value = dict[key] else { return nil }
56+
return value.toFieldInputData(_variables: _variables)
57+
case .json(let dict):
58+
guard let value = dict[key] else {
59+
return nil
60+
}
61+
return jsonValueToFieldInputData(value)
62+
}
63+
}
64+
65+
public enum RawType {
66+
case inputValue([String: InputValue])
67+
case json(JSONObject)
68+
}
69+
}
70+
71+
/// An opaque wrapper for input data of a GraphQL operation. This type wraps data from the
72+
/// arguments and variables for the given field/operation to provide an easy way to work
73+
/// with that data to create cache keys.
74+
public struct InputListData {
75+
public let _rawType: RawType
76+
public let _variables: GraphQLOperation.Variables?
77+
public let count: Int
78+
79+
public init(
80+
_rawType: RawType,
81+
_variables: GraphQLOperation.Variables?
82+
) {
83+
self._rawType = _rawType
84+
self._variables = _variables
85+
86+
switch _rawType {
87+
case .hashable(let list):
88+
self.count = list.count
89+
case .inputValue(let list):
90+
self.count = list.count
91+
}
92+
}
93+
94+
@inlinable public subscript(_ index: Int) -> (any ScalarType)? {
95+
switch _rawType {
96+
case .hashable(let list):
97+
let value: JSONValue = list[index]
98+
return jsonValueToAnyScalar(value)
99+
case .inputValue(let list):
100+
let value = list[index]
101+
return value.toAnyScalar(_variables: _variables)
102+
}
103+
}
104+
105+
@_disfavoredOverload
106+
@inlinable public subscript(_ index: Int) -> InputData? {
107+
switch _rawType {
108+
case .hashable(let list):
109+
let value = list[index]
110+
return jsonValueToFieldInputData(value)
111+
case .inputValue(let list):
112+
let value = list[index]
113+
return value.toFieldInputData(_variables: _variables)
114+
}
115+
}
116+
117+
@_disfavoredOverload
118+
@inlinable public subscript(_ index: Int) -> InputListData? {
119+
switch _rawType {
120+
case .hashable(let list):
121+
let value = list[index]
122+
return jsonValueToFieldInputListData(value)
123+
case .inputValue(let list):
124+
let value = list[index]
125+
return value.toFieldInputListData(_variables: _variables)
126+
}
127+
}
128+
129+
public enum RawType {
130+
case hashable([JSONValue])
131+
case inputValue([InputValue])
132+
}
133+
}
134+
135+
// MARK: - JSONValue Helper Functions
136+
137+
@usableFromInline static func jsonValueToAnyScalar(_ val: any Hashable & Sendable) -> (any ScalarType)? {
138+
var value = val
139+
switch value {
140+
case let boolVal as Bool:
141+
value = boolVal
142+
case let intVal as any FixedWidthInteger:
143+
value = Int32(intVal)
144+
case let str as NSString:
145+
value = str as String
146+
default:
147+
break
148+
}
149+
150+
switch value {
151+
case let scalar as any ScalarType:
152+
return scalar
153+
case let customScalar as any CustomScalarType:
154+
return customScalar._jsonValue as? (any ScalarType)
155+
default:
156+
return nil
157+
}
158+
}
159+
160+
@usableFromInline static func jsonValueToFieldInputListData(_ val: JSONValue) -> FieldPolicy.InputListData? {
161+
if let list = val as? [JSONValue] {
162+
return FieldPolicy.InputListData(_rawType: .hashable(list), _variables: nil)
163+
}
164+
return nil
165+
}
166+
167+
@usableFromInline static func jsonValueToFieldInputData(_ val: JSONValue) -> FieldPolicy.InputData? {
168+
if let object = val as? JSONObject {
169+
return FieldPolicy.InputData(_rawType: .json(object), _variables: nil)
170+
}
171+
return nil
172+
}
173+
}
174+
175+
extension InputValue {
176+
@usableFromInline func toAnyScalar(_variables: GraphQLOperation.Variables?) -> (any ScalarType)? {
177+
switch self {
178+
case .scalar(let scalar):
179+
return scalar
180+
case .variable(let varName):
181+
guard let varValue = _variables?[varName] else { return nil }
182+
return varValue._jsonEncodableValue?._jsonValue as? (any ScalarType)
183+
default:
184+
return nil
185+
}
186+
}
187+
188+
@usableFromInline func toFieldInputListData(_variables: GraphQLOperation.Variables?) -> FieldPolicy.InputListData? {
189+
switch self {
190+
case .list(let list):
191+
return FieldPolicy.InputListData(_rawType: .inputValue(list), _variables: _variables)
192+
case .variable(let varName):
193+
guard let varValue = _variables?[varName],
194+
let list = varValue._jsonEncodableValue?._jsonValue as? [JSONValue] else {
195+
return nil
196+
}
197+
return FieldPolicy.InputListData(_rawType: .hashable(list), _variables: _variables)
198+
default:
199+
return nil
200+
}
201+
}
202+
203+
@usableFromInline func toFieldInputData(_variables: GraphQLOperation.Variables?) -> FieldPolicy.InputData? {
204+
switch self {
205+
case .object(let object):
206+
return FieldPolicy.InputData(_rawType: .inputValue(object), _variables: _variables)
207+
case .variable(let varName):
208+
guard let varValue = _variables?[varName],
209+
let object = varValue._jsonEncodableValue?._jsonValue as? JSONObject else {
210+
return nil
211+
}
212+
return FieldPolicy.InputData(_rawType: .json(object), _variables: _variables)
213+
default:
214+
return nil
215+
}
216+
}
217+
}

0 commit comments

Comments
 (0)