Skip to content

Commit

Permalink
feat: Specify caching fields with typePolicy directive
Browse files Browse the repository at this point in the history
  • Loading branch information
x-sheep committed Dec 6, 2024
1 parent c694236 commit 3d8326b
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ input PetSearchFilters {
measurements: MeasurementsInput
}

interface Animal {
interface Animal @typePolicy(keyFields: "id") {
id: ID!
species: String!
height: Height!
predators: [Animal!]!
skinCovering: SkinCovering
}

interface Pet {
interface Pet @typePolicy(keyFields: "id") {
id: ID!
humanName: String
favoriteToy: String!
Expand Down Expand Up @@ -198,5 +198,5 @@ enum SkinCovering {
FUR
HAIR
FEATHERS
SCALES
SCALES
}
4 changes: 3 additions & 1 deletion Tests/ApolloCodegenInternalTestHelpers/MockGraphQLType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public extension GraphQLObjectType {
_ name: String = "",
interfaces: [GraphQLInterfaceType] = [],
fields: [String: GraphQLField] = [:],
keyFields: [String] = [],
documentation: String? = nil
) -> GraphQLObjectType {
GraphQLObjectType(
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 @@ -20,12 +20,14 @@ class ObjectTemplateTests: XCTestCase {
name: String = "Dog",
customName: String? = nil,
interfaces: [GraphQLInterfaceType] = [],
keyFields: [String] = [],
documentation: String? = nil,
config: ApolloCodegenConfiguration = .mock()
) {
let objectType = GraphQLObjectType.mock(
name,
interfaces: interfaces,
keyFields: keyFields,
documentation: documentation
)
objectType.name.customName = customName
Expand Down Expand Up @@ -82,7 +84,7 @@ class ObjectTemplateTests: XCTestCase {
implementedInterfaces: [
TestSchema.Interfaces.Animal.self,
TestSchema.Interfaces.Pet.self
]
],
"""

// when
Expand All @@ -106,7 +108,7 @@ class ObjectTemplateTests: XCTestCase {
implementedInterfaces: [
Interfaces.Animal.self,
Interfaces.Pet.self
]
],
"""

// when
Expand All @@ -121,7 +123,7 @@ class ObjectTemplateTests: XCTestCase {
buildSubject()

let expected = """
implementedInterfaces: []
implementedInterfaces: [],
"""

// when
Expand All @@ -130,6 +132,21 @@ class ObjectTemplateTests: XCTestCase {
// then
expect(actual).to(equalLineByLine(expected, atLine: 3, ignoringExtraLines: true))
}

func test_render_givenKeyFields_rendersKeyFieldArray() {
// given
buildSubject(keyFields: ["id"])

let expected = """
keyFields: ["id"]
"""

// when
let actual = renderSubject()

// then
expect(actual).to(equalLineByLine(expected, atLine: 4, ignoringExtraLines: true))
}

// MARK: Documentation Tests

Expand Down Expand Up @@ -213,7 +230,8 @@ class ObjectTemplateTests: XCTestCase {
// Renamed from GraphQL schema value: 'MyObject'
static let MyCustomObject = ApolloAPI.Object(
typename: "MyObject",
implementedInterfaces: [TestSchema.Interfaces.MyCustomInterface.self]
implementedInterfaces: [TestSchema.Interfaces.MyCustomInterface.self],
keyFields: nil
)
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4794,7 +4794,7 @@ class SelectionSetTemplateTests: XCTestCase {
AllAnimals: [Animal!]
}
type Animal {
type Animal @typePolicy(keyFields: "id") {
id: ID!
}
"""
Expand Down
51 changes: 51 additions & 0 deletions Tests/ApolloTests/CacheKeyResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,55 @@ class CacheKeyResolutionTests: XCTestCase {
expect(actual).to(equal("GreekLetters:δ"))
}

func test__schemaConfiguration__givenSingleKeyField_shouldReturnKeyFieldValue() {
let Delta = Object(typename: "Dog", implementedInterfaces: [], keyFields: ["id"])

MockSchemaMetadata.stub_objectTypeForTypeName({ _ in Delta })

let object: JSONObject = [
"__typename": "Dog",
"id": "10",
"name": "Beagle"
]

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

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

func test__schemaConfiguration__givenMultipleKeyFields_shouldReturnKeyFieldValues() {
let Delta = Object(typename: "Dog", implementedInterfaces: [], keyFields: ["id", "name"])

MockSchemaMetadata.stub_objectTypeForTypeName({ _ in Delta })

let object: JSONObject = [
"__typename": "Dog",
"id": "10",
"name": "Beagle",
"height": 20,
]

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

expect(actual).to(equal("Dog:10/Beagle"))
}

func test__schemaConfiguration__givenMissingKeyFields_shouldReturnNil() {
let Delta = Object(typename: "Dog", implementedInterfaces: [], keyFields: ["id", "name"])

MockSchemaMetadata.stub_objectTypeForTypeName({ _ in Delta })

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

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

expect(actual).to(beNil())
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ struct ObjectTemplate: TemplateRenderer {
\(graphqlObject.name.typeNameDocumentation)
static let \(graphqlObject.render(as: .typename)) = \(config.ApolloAPITargetName).Object(
typename: "\(graphqlObject.name.schemaName)\",
implementedInterfaces: \(ImplementedInterfacesTemplate())
implementedInterfaces: \(ImplementedInterfacesTemplate()),
keyFields: \(KeyFieldsTemplate())
)
"""
}

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

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

private func ImplementedInterfacesTemplate() -> TemplateString {
return """
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,20 @@ public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImpl
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 @@ -259,6 +263,7 @@ public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImpl
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 @@ -96,4 +96,4 @@ export type InclusionConditionSkipped = "SKIPPED";
export interface InclusionConditionVariable {
variable: string;
isInverted: Boolean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import { assertValidSchema, assertValidSDL } from "./utilities/graphql";
import {
addApolloCodegenSchemaExtensionToDocument,
} from "./utilities/apolloCodegenSchemaExtension";
import {
import {
addExperimentalDeferDirectiveToSDLDocument,
addExperimentalDeferDirectiveToIntrospectionSchema
} from "./utilities/experimentalDeferDirective";
import { addTypePolicyDirectivesToSchema } from "./utilities/typePolicyResolver";

// We need to export all the classes we want to map to native objects,
// so we have access to the constructor functions for type checks.
Expand Down Expand Up @@ -61,6 +62,7 @@ export function loadSchemaFromSources(sources: Source[]): GraphQLSchema {
assertValidSDL(document)

const schema = buildASTSchema(document, { assumeValid: true, assumeValidSDL: true })
addTypePolicyDirectivesToSchema(schema)
assertValidSchema(schema)

return schema
Expand All @@ -69,6 +71,7 @@ export function loadSchemaFromSources(sources: Source[]): GraphQLSchema {
var schema = loadSchemaFromIntrospectionResult(introspectionJSONResult.body)
document = addExperimentalDeferDirectiveToIntrospectionSchema(schema, document)
schema = extendSchema(schema, document, { assumeValid: true, assumeValidSDL: true })
addTypePolicyDirectivesToSchema(schema)

assertValidSchema(schema)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ export const directive_apollo_client_ios_localCacheMutation: DirectiveDefinition
locations: [nameNode("QUERY"), nameNode("MUTATION"), nameNode("SUBSCRIPTION"), nameNode("FRAGMENT_DEFINITION")]
}

export const directive_typePolicy: DirectiveDefinitionNode = {
kind: Kind.DIRECTIVE_DEFINITION,
description: stringNode("Attach extra information to a given type."),
name: nameNode("typePolicy"),
arguments: [
{
kind: Kind.INPUT_VALUE_DEFINITION,
description: stringNode("A selection set containing fields used to compute the cache key of an object. Order is important."),
name: nameNode("keyFields"),
type: nonNullNode(typeNode(GraphQLString))
}
],
repeatable: false,
locations: [nameNode("OBJECT"), nameNode("INTERFACE")]
}

export const directive_import_statement: DirectiveDefinitionNode = {
kind: Kind.DIRECTIVE_DEFINITION,
description: stringNode("A directive used by the Apollo iOS code generation engine to generate custom import statements in operation or fragment definition files. An import statement to import a module with the name provided in the `module` argument will be added to the generated definition file."),
Expand All @@ -29,7 +45,8 @@ export const apolloCodegenSchemaExtension: DocumentNode = {
kind: Kind.DOCUMENT,
definitions: [
directive_apollo_client_ios_localCacheMutation,
directive_import_statement
directive_import_statement,
directive_typePolicy,
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "graphql";
import { isNode } from "graphql/language/ast";
import { validateSDL } from "graphql/validation/validate";
import { directive_apollo_client_ios_localCacheMutation, directive_import_statement } from "./apolloCodegenSchemaExtension";
import { directive_apollo_client_ios_localCacheMutation, directive_import_statement, directive_typePolicy } from "./apolloCodegenSchemaExtension";
import { addTypeNameFieldForLegacySafelisting } from "./legacySafelistingTransform";

export class GraphQLSchemaValidationError extends Error {
Expand Down Expand Up @@ -124,7 +124,8 @@ function transformTypenameFieldIfNeeded(node: FieldNode): FieldNode {
function stripApolloClientSpecificDirectives(node: DirectiveNode): DirectiveNode | null {
const apolloClientSpecificDirectives: DirectiveDefinitionNode[] = [
directive_apollo_client_ios_localCacheMutation,
directive_import_statement
directive_import_statement,
directive_typePolicy,
]

for (const directive of apolloClientSpecificDirectives) {
Expand Down
Loading

0 comments on commit 3d8326b

Please sign in to comment.