Skip to content

Commit

Permalink
Add keyFields to generated Interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
x-sheep committed Dec 17, 2024
1 parent c9c78c4 commit c40b446
Show file tree
Hide file tree
Showing 14 changed files with 96 additions and 20 deletions.
4 changes: 3 additions & 1 deletion Tests/ApolloCodegenInternalTestHelpers/MockGraphQLType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ public extension GraphQLInterfaceType {
class func mock(
_ name: String = "",
fields: [String: GraphQLField] = [:],
keyFields: [String] = [],
interfaces: [GraphQLInterfaceType] = [],
documentation: String? = nil
) -> GraphQLInterfaceType {
GraphQLInterfaceType(
name: GraphQLName(schemaName: name),
documentation: documentation,
fields: fields,
interfaces: interfaces
interfaces: interfaces,
keyFields: keyFields
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ final class AnimalKingdomIRCreationTests: XCTestCase {

func test__mergedSelections_AllAnimalsQuery_AllAnimal__isCorrect() async throws {
// given
let Interface_Animal = GraphQLInterfaceType.mock("Animal")
let Interface_Animal = GraphQLInterfaceType.mock("Animal", keyFields: ["id"])

try await buildOperation()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ class InterfaceTemplateTests: XCTestCase {
name: String = "Dog",
customName: String? = nil,
documentation: String? = nil,
keyFields: [String] = [],
config: ApolloCodegenConfiguration = .mock()
) {
let interfaceType = GraphQLInterfaceType.mock(
name,
fields: [:],
keyFields: keyFields,
interfaces: [],
documentation: documentation
)
Expand All @@ -46,7 +48,7 @@ class InterfaceTemplateTests: XCTestCase {
buildSubject(name: "aDog")

let expected = """
static let ADog = ApolloAPI.Interface(name: "aDog")
static let ADog = ApolloAPI.Interface(name: "aDog", keyFields: nil)
"""

// when
Expand All @@ -68,7 +70,7 @@ class InterfaceTemplateTests: XCTestCase {

let expected = """
/// \(documentation)
static let Dog = ApolloAPI.Interface(name: "Dog")
static let Dog = ApolloAPI.Interface(name: "Dog", keyFields: nil)
"""

// when
Expand All @@ -88,7 +90,7 @@ class InterfaceTemplateTests: XCTestCase {
)

let expected = """
static let Dog = ApolloAPI.Interface(name: "Dog")
static let Dog = ApolloAPI.Interface(name: "Dog", keyFields: nil)
"""

// when
Expand All @@ -108,7 +110,7 @@ class InterfaceTemplateTests: XCTestCase {
)

let expected = """
static let Dog = Apollo.Interface(name: "Dog")
static let Dog = Apollo.Interface(name: "Dog", keyFields: nil)
"""

// when
Expand All @@ -128,7 +130,7 @@ class InterfaceTemplateTests: XCTestCase {
buildSubject(name: keyword)

let expected = """
static let \(keyword.firstUppercased)_Interface = ApolloAPI.Interface(name: "\(keyword)")
static let \(keyword.firstUppercased)_Interface = ApolloAPI.Interface(name: "\(keyword)", keyFields: nil)
"""

// when
Expand All @@ -150,7 +152,28 @@ class InterfaceTemplateTests: XCTestCase {

let expected = """
// Renamed from GraphQL schema value: 'MyInterface'
static let MyCustomInterface = ApolloAPI.Interface(name: "MyInterface")
static let MyCustomInterface = ApolloAPI.Interface(name: "MyInterface", keyFields: nil)
"""

// when
let actual = renderSubject()

// then
expect(actual).to(equalLineByLine(expected))
}

func test__render__givenInterface_withKeyFields_shouldRenderKeyFields() throws {
// given
buildSubject(
name: "IndexedNode",
keyFields: ["parentID", "index"]
)

let expected = """
static let IndexedNode = ApolloAPI.Interface(name: "IndexedNode", keyFields: [
"parentID",
"index"
])
"""

// when
Expand Down
14 changes: 14 additions & 0 deletions Tests/ApolloTests/CacheKeyResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,18 @@ class CacheKeyResolutionTests: XCTestCase {
expect(actual).to(beNil())
}

func test__schemaConfiguration__givenInterfaceWithKeyField_shouldReturnKeyFieldValue() {
let Interface = Interface(name: "Animal", keyFields: ["id"])

let object: JSONObject = [
"__typename": "Cat",
"id": "10",
]

let objectDict = NetworkResponseExecutionSource().opaqueObjectDataWrapper(for: object)
let actual = MockSchemaMetadata.cacheKey(for: objectDict, withInterface: Interface)

expect(actual).to(equal("Cat:10"))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ struct InterfaceTemplate: TemplateRenderer {
"""
\(documentation: graphqlInterface.documentation, config: config)
\(graphqlInterface.name.typeNameDocumentation)
static let \(graphqlInterface.render(as: .typename)) = \(config.ApolloAPITargetName).Interface(name: "\(graphqlInterface.name.schemaName)")
static let \(graphqlInterface.render(as: .typename)) = \(config.ApolloAPITargetName).Interface(name: "\(graphqlInterface.name.schemaName)", keyFields: \(KeyFieldsTemplate()))
"""
}

private func KeyFieldsTemplate() -> TemplateString {
guard let fields = graphqlInterface.keyFields, !fields.isEmpty else { return "nil" }

return """
[\(list: fields.map { "\"\($0)\"" })]
"""
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -279,16 +279,20 @@ public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceIm
public private(set) var fields: [String: GraphQLField]!

public private(set) var interfaces: [GraphQLInterfaceType]!

public private(set) var keyFields: [String]!

/// Initializer to be used for creating mock objects in tests only.
init(
name: GraphQLName,
documentation: String?,
fields: [String: GraphQLField],
interfaces: [GraphQLInterfaceType]
interfaces: [GraphQLInterfaceType],
keyFields: [String]
) {
self.fields = fields
self.interfaces = interfaces
self.keyFields = keyFields
super.init(name: name, documentation: documentation)
}

Expand All @@ -299,6 +303,7 @@ public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceIm
override func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) {
self.fields = try! bridge.invokeMethod("getFields", on: jsValue)
self.interfaces = try! bridge.invokeMethod("getInterfaces", on: jsValue)
self.keyFields = jsValue["_apolloKeyFields"]
}

public override var debugDescription: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ describe("given SDL without typePolicy", () => {
allRectangles: [Rectangle!]
}
interface Shape {
surface: Int!
}
type Rectangle {
width: Int!
height: Int!
Expand All @@ -29,6 +33,9 @@ describe("given SDL without typePolicy", () => {

const type = schema.getTypeMap()["Rectangle"] as ObjectWithMeta;
expect(type._apolloKeyFields).toHaveLength(0);

const iface = schema.getTypeMap()["Shape"] as ObjectWithMeta;
expect(iface._apolloKeyFields).toHaveLength(0);
});
});

Expand Down Expand Up @@ -86,6 +93,10 @@ describe("given SDL with valid typePolicy", () => {
const type = schema.getTypeMap()["Dog"] as ObjectWithMeta;
expect(type._apolloKeyFields).toHaveLength(1);
expect(type._apolloKeyFields).toContain("id");

const iface = schema.getTypeMap()["Animal"] as ObjectWithMeta;
expect(iface._apolloKeyFields).toHaveLength(1);
expect(iface._apolloKeyFields).toContain("id");
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export function addTypePolicyDirectivesToSchema(schema: GraphQLSchema) {
for (const key in types) {
const type = types[key];

if (type instanceof GraphQLObjectType) {
if (type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType) {
(type as any)._apolloKeyFields = keyFieldsFor(type);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ 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, withInterface interface: Interface?) -> CacheKey? {
return object.key
}

Expand Down
6 changes: 3 additions & 3 deletions apollo-ios/Sources/Apollo/GraphQLExecutionSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public protocol GraphQLExecutionSource {
/// - 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, withInterface interface: Interface?) -> CacheKey?
}

/// A type of `GraphQLExecutionSource` that uses the user defined cache key computation
Expand All @@ -57,8 +57,8 @@ 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, withInterface interface: Interface?) -> CacheKey? {
let dataWrapper = opaqueObjectDataWrapper(for: object)
return schema.cacheKey(for: dataWrapper)
return schema.cacheKey(for: dataWrapper, withInterface: interface)
}
}
4 changes: 3 additions & 1 deletion apollo-ios/Sources/Apollo/GraphQLExecutor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -493,9 +493,11 @@ 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, withInterface: expectedInterface)
)

return execute(
Expand Down
5 changes: 3 additions & 2 deletions apollo-ios/Sources/ApolloAPI/SchemaMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,17 @@ extension SchemaMetadata {
/// for the `NormalizedCache`.
///
/// - Parameter object: A ``JSONObject`` dictionary representing an object in a GraphQL response.
/// - Parameter interface: An optional ``Interface`` of the expected object.
/// - 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? {
@inlinable public static func cacheKey(for object: ObjectData, withInterface interface: 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 else { return nil }
guard let keyFields = type.keyFields ?? interface?.keyFields else { return nil }

let idValues = try? keyFields.map {
guard let keyFieldValue = object[$0] else {
Expand Down
12 changes: 11 additions & 1 deletion apollo-ios/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
}
}
}

0 comments on commit c40b446

Please sign in to comment.