Skip to content

Commit

Permalink
feat: Specify caching fields with typePolicy directive (apollographql…
Browse files Browse the repository at this point in the history
  • Loading branch information
x-sheep authored and gh-action-runner committed Jan 22, 2025
1 parent 9ec4b0b commit f38dd44
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ struct CacheDataExecutionSource: GraphQLExecutionSource {
return transaction.loadObject(forKey: reference.key)
}

func computeCacheKey(for object: Record, in schema: any SchemaMetadata.Type) -> CacheKey? {
func computeCacheKey(
for object: Record,
in schema: any SchemaMetadata.Type,
inferredToImplementInterface interface: Interface?
) -> CacheKey? {
return object.key
}

Expand Down
18 changes: 15 additions & 3 deletions Sources/Apollo/GraphQLExecutionSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ public protocol GraphQLExecutionSource {
/// - Parameters:
/// - object: The data for the object from the source.
/// - schema: The schema that the type the object data represents belongs to.
/// - implementedInterface: An optional ``Interface`` that the object is
/// inferred to implement. If the cache key is being resolved for a selection set with an
/// interface as it's `__parentType`, you can infer the object must implement that interface.
/// You should provide that interface to this parameter.
/// - Returns: A cache key for normalizing the object in the cache. If `nil` is returned the
/// object is assumed to be stored in the cache with no normalization. The executor will
/// construct a cache key based on the object's path in its enclosing operation.
func computeCacheKey(for object: RawObjectData, in schema: any SchemaMetadata.Type) -> CacheKey?
func computeCacheKey(
for object: RawObjectData,
in schema: any SchemaMetadata.Type,
inferredToImplementInterface implementedInterface: Interface?
) -> CacheKey?
}

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

extension CacheKeyComputingExecutionSource {
@_spi(Execution) public func computeCacheKey(for object: RawObjectData, in schema: any SchemaMetadata.Type) -> CacheKey? {
@_spi(Execution) public func computeCacheKey(
for object: RawObjectData,
in schema: any SchemaMetadata.Type,
inferredToImplementInterface implementedInterface: Interface?
) -> CacheKey? {
let dataWrapper = opaqueObjectDataWrapper(for: object)
return schema.cacheKey(for: dataWrapper)
return schema.cacheKey(for: dataWrapper, inferredToImplementInterface: implementedInterface)
}
}
8 changes: 7 additions & 1 deletion Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,9 +493,15 @@ public final class GraphQLExecutor<Source: GraphQLExecutionSource> {
onChildObject object: Source.RawObjectData,
accumulator: Accumulator
) -> PossiblyDeferred<Accumulator.PartialResult> {
let expectedInterface = rootSelectionSetType.__parentType as? Interface

let (childExecutionInfo, selections) = fieldInfo.computeChildExecutionData(
withRootType: rootSelectionSetType,
cacheKey: executionSource.computeCacheKey(for: object, in: fieldInfo.parentInfo.schema)
cacheKey: executionSource.computeCacheKey(
for: object,
in: fieldInfo.parentInfo.schema,
inferredToImplementInterface: expectedInterface
)
)

return execute(
Expand Down
55 changes: 45 additions & 10 deletions Sources/ApolloAPI/SchemaMetadata.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

/// A protocol that a generated GraphQL schema should conform to.
///
/// The generated schema metadata is the source of information about the generated types in the
Expand Down Expand Up @@ -43,20 +45,53 @@ extension SchemaMetadata {
/// Resolves the cache key for an object in a GraphQL response to be used by
/// `NormalizedCache` mechanisms.
///
/// Maps the type of the `object` using the ``graphQLType(for:)`` function, then gets the
/// ``CacheKeyInfo`` for the `object` using the ``SchemaConfiguration/cacheKeyInfo(for:object:)``
/// function.
/// Finally, this function transforms the ``CacheKeyInfo`` into the correct ``CacheReference``
/// for the `NormalizedCache`.
/// The algorithm for resolving the objects cache key:
/// 1. Map the type of the `object` using the ``graphQLType(for:)`` function.
/// 2. Attempt to gets the `CacheKeyInfo`` using programmatic cache key configuration.
/// 2a. Call the ``SchemaConfiguration/cacheKeyInfo(for:object:)`` function.
/// 2b. If `CacheKeyInfo` is found, transforms the ``CacheKeyInfo`` into the correct ``CacheReference``
/// for the `NormalizedCache` and return it.
/// 3. If no programmatic cache key is returned, attempt to resolve the `keyFields` for the object
/// 3a. Check if the object's type has `keyFields`.
/// 3b. If the type of the object is unknown (ie. it cannot be found by ``graphQLType(for:)``),
/// or the type does not have `keyFields`, check if the inferred interface for the type has
/// `keyFields`.
/// 3c. If `keyFields` are found, resolve the cache key by escaping and joining the values of
/// the `keyFields` on the object. Return the resolved cache key.
/// 4. If a cache key is not resolved programmatically or using `keyFields`, return `nil`.
///
/// - Parameter object: A ``JSONObject`` dictionary representing an object in a GraphQL response.
/// - Parameter implementedInterface: An optional ``Interface`` that the object is
/// inferred to implement. If the cache key is being resolved for a selection set with an
/// interface as it's `__parentType`, you can infer the object must implement that interface.
/// You should provide that interface to this parameter.
///
/// - Returns: A `String` representing the cache key for the `object` to be used by
/// `NormalizedCache` mechanisms.
@inlinable public static func cacheKey(for object: ObjectData) -> String? {
guard let type = graphQLType(for: object),
let info = configuration.cacheKeyInfo(for: type, object: object) else {
return nil
@inlinable public static func cacheKey(
for object: ObjectData,
inferredToImplementInterface implementedInterface: Interface? = nil
) -> String? {
guard let type = graphQLType(for: object) else { return nil }

if let info = configuration.cacheKeyInfo(for: type, object: object) {
return "\(info.uniqueKeyGroup ?? type.typename):\(info.id)"
}

guard let keyFields = type.keyFields ?? implementedInterface?.keyFields else { return nil }

let idValues = try? keyFields.map {
guard let keyFieldValue = object[$0] else {
throw JSONDecodingError.missingValue
}
let item = try String(_jsonValue: keyFieldValue._asAnyHashable)

// Escape all instances of `+` with a backslash, as well as other backslashes
return item.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "+", with: "\\+")
}
return "\(info.uniqueKeyGroup ?? type.typename):\(info.id)"

guard let id = idValues?.joined(separator: "+") else { return nil }
return "\(type.typename):\(id)"
}
}
12 changes: 11 additions & 1 deletion Sources/ApolloAPI/SchemaTypes/Interface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@
public struct Interface: Hashable, Sendable {
/// The name of the ``Interface`` in the GraphQL schema.
public let name: String

/// A list of fields used to uniquely identify an instance of an object implementing this interface.
///
/// This is set by adding a `@typePolicy` directive to the schema.
public let keyFields: [String]?

/// Designated Initializer
///
/// - Parameter name: The name of the ``Interface`` in the GraphQL schema.
public init(name: String) {
public init(name: String, keyFields: [String]? = nil) {
self.name = name
if keyFields?.isEmpty == false {
self.keyFields = keyFields
} else {
self.keyFields = nil
}
}
}
14 changes: 13 additions & 1 deletion Sources/ApolloAPI/SchemaTypes/Object.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ public struct Object: Hashable, Sendable {
/// - Parameters:
/// - typename: The name of the type.
/// - implementedInterfaces: A list of the interfaces implemented by the type.
/// - keyFields: A list of field names that are used to uniquely identify an instance of this type.
public init(
typename: String,
implementedInterfaces: [Interface]
implementedInterfaces: [Interface],
keyFields: [String]? = nil
) {
self.typename = typename
self.implementedInterfaces = implementedInterfaces
if keyFields?.isEmpty == false {
self.keyFields = keyFields
} else {
self.keyFields = nil
}
}

/// A list of the interfaces implemented by the type.
Expand All @@ -26,6 +33,11 @@ public struct Object: Hashable, Sendable {
/// When an entity of the type is included in a GraphQL response its `__typename` field will
/// match this value.
public let typename: String

/// A list of fields used to uniquely identify an instance of this object.
///
/// This is set by adding a `@typePolicy` directive to the schema.
public let keyFields: [String]?

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

0 comments on commit f38dd44

Please sign in to comment.