From b3cd87b7544a7063ba48935ee679051fdacf0f95 Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Wed, 27 Nov 2024 14:59:25 +0100 Subject: [PATCH 1/5] rfc: Conformance to Identifiable protocol --- apollo-ios/Design/swift-identifiable.md | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 apollo-ios/Design/swift-identifiable.md diff --git a/apollo-ios/Design/swift-identifiable.md b/apollo-ios/Design/swift-identifiable.md new file mode 100644 index 000000000..2f3265b9e --- /dev/null +++ b/apollo-ios/Design/swift-identifiable.md @@ -0,0 +1,94 @@ +# Summary + +This document proposes a new GraphQL directive named `@apollo_client_ios_identity`, to be used to mark a field which can uniquely identify an object. + +# Introduction + +SwiftUI makes heavy use of the [Identifiable](https://developer.apple.com/documentation/swift/identifiable) protocol, which is used to track the identity of entities across data changes. If an object with a List is replaced with a different object with the same identity, SwiftUI will animate the item changing instead of animating an insertion. Objects that do not conform to the Identifiable protocol require additional boilerplate to be usable inside SwiftUI. + +Apollo Client iOS could assist the developer by adding conformance to the Identifiable protocol to its generated models. Selecting the field to be used as an identity is done by adding a new GraphQL directive. + +# Definition + +The directive is defined as: +```graphql +directive @apollo_client_ios_identity on FIELD | FIELD_DEFINITION +``` + +This directive MUST be used on a field with a non-nullable scalar type. + +## Usage in types + +A type MAY have a field annotated with the directive. + +```graphql +type Animal { + id: ID! @apollo_client_ios_identity + name: String +} +``` + +Within a type, the directive MUST NOT be used on more than one field. + +## Usage in interfaces + +It's possible for interfaces to use the directive on a field. + +```graphql +interface Identifiable { + id: ID! @apollo_client_ios_identity +} +``` + +Implementations of this interface MUST copy the directive to the same field. + +## Usage in operations + +It's likely that an external schema will not use this directive. In this case, an identity MAY be chosen when writing a query. The directive does not need to be included in the query sent to the GraphQL server. + +```graphql +query GetAllAnimals { + allAnimals { + id @apollo_client_ios_identity + string + } +} +``` + +It is allowed for a query and a type to both use the directive to describe the same field. + +If the server defines a field as an identity, a query SHOULD NOT choose another field. A query MUST NOT use the directive on more than one field in the same selection (unless they are in differently nested objects). + +## Generated code + +If an operation contains a field marked with the `@apollo_client_ios_identity` directive (by either the schema or the operation itself), the generated SelectionSet will have a conformance to the Identifiable protocol. + +```swift +// Inline selection +public struct Data: AnimalKingdomAPI.SelectionSet, Identifiable { /* ... */ } + +// Fragment selection +public struct PetDetails: AnimalKingdomAPI.SelectionSet, Fragment, Identifiable { /* ... */ } +``` + +The protocol requires that the identity is accessible through a public field named exactly `id`. If the annotated field has a different name, an additional getter will be generated: + +```swift +public var id: String { self.uuid } +``` + +## Naming conflicts + +If the identity field is not called `id`, but another field called `id` is present in the selection, a custom getter cannot be added. Swift does not support using another field to handle the conformance. + +In this case, a conformance to Identifiable SHOULD NOT be generated. + +# Alternatives + +## Automatic conformance to Identifiable + +The code generator could be updated to always emit a conformance to Identifiable if a scalar `id` field is present in the selection set, removing the need for a custom directive. This was suggested in Pull Request [#548](https://github.com/apollographql/apollo-ios-dev/pull/548). + +## Declaring identity scope + +The directive could be expanded to declare the scope of an identity (e.g. unique across the entire API or only one specific Database table). This makes the directive beneficial outside of Swift code generation, but it doesn't change the behavior of the Identifiable protocol. The [Swift documentation](https://developer.apple.com/documentation/swift/identifiable) states that the scope and duration of an identity is unspecified, so modifying the scope would not lead to a difference in the generated code. From 4656a47462c983af1ed9f83e3eb210ba5454b131 Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Wed, 27 Nov 2024 21:30:46 +0100 Subject: [PATCH 2/5] Update swift-identifiable.md --- apollo-ios/Design/swift-identifiable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-ios/Design/swift-identifiable.md b/apollo-ios/Design/swift-identifiable.md index 2f3265b9e..694060439 100644 --- a/apollo-ios/Design/swift-identifiable.md +++ b/apollo-ios/Design/swift-identifiable.md @@ -81,7 +81,7 @@ public var id: String { self.uuid } If the identity field is not called `id`, but another field called `id` is present in the selection, a custom getter cannot be added. Swift does not support using another field to handle the conformance. -In this case, a conformance to Identifiable SHOULD NOT be generated. +In this case, a conformance to Identifiable SHOULD NOT be generated. Codegen should emit a warning without stopping the generation process. # Alternatives From 6430f37b1cc2931dd5236c4087a78e31a3ecffb8 Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Thu, 28 Nov 2024 14:55:00 +0100 Subject: [PATCH 3/5] Add scope parameter --- apollo-ios/Design/swift-identifiable.md | 60 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/apollo-ios/Design/swift-identifiable.md b/apollo-ios/Design/swift-identifiable.md index 694060439..d3f8e234b 100644 --- a/apollo-ios/Design/swift-identifiable.md +++ b/apollo-ios/Design/swift-identifiable.md @@ -1,6 +1,6 @@ # Summary -This document proposes a new GraphQL directive named `@apollo_client_ios_identity`, to be used to mark a field which can uniquely identify an object. +This document proposes a new GraphQL directive named `@identity`, to be used to mark a field which can uniquely identify an object. # Introduction @@ -8,14 +8,32 @@ SwiftUI makes heavy use of the [Identifiable](https://developer.apple.com/docume Apollo Client iOS could assist the developer by adding conformance to the Identifiable protocol to its generated models. Selecting the field to be used as an identity is done by adding a new GraphQL directive. +A concept of identity is required to allow response objects to be cached. The various Apollo Client projects have mechanisms that allow for identifiers to be selected through additional code. The new directive could allow schema authors to assist client authors with caching. + # Definition The directive is defined as: ```graphql -directive @apollo_client_ios_identity on FIELD | FIELD_DEFINITION +directive @identity(scope: IdentityScope = SELECTION) on FIELD | FIELD_DEFINITION + +enum IdentityScope { + SELECTION + TYPE + SERVICE + GLOBAL +} ``` -This directive MUST be used on a field with a non-nullable scalar type. +This directive MUST be used on a field with a non-nullable scalar type. Other types are not supported. + +## Scope parameter + +The directive can have a parameter indicating the scope of the identity. They are ordered from _narrowest_ to _widest_: + +1. `SELECTION`: The identifier is only unique within the current list. Identifiers may be reused between different objects with the same typename. +2. `TYPE`: The identifier is unique for the type, e.g. an auto-incrementing column from a database table. +3. `SERVICE`: The identifier is unique across the current GraphQL Service. +4. `GLOBAL`: The identifier is unique across all GraphQL services. This can be used by identifiers generated according to [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) or [RFC 9562](https://datatracker.ietf.org/doc/html/rfc9562) (also known as Universally Unique IDentifiers or UUIDs). ## Usage in types @@ -23,7 +41,7 @@ A type MAY have a field annotated with the directive. ```graphql type Animal { - id: ID! @apollo_client_ios_identity + id: ID! @identity(scope: SERVICE) name: String } ``` @@ -36,32 +54,32 @@ It's possible for interfaces to use the directive on a field. ```graphql interface Identifiable { - id: ID! @apollo_client_ios_identity + id: ID! @identity(scope: TYPE) } ``` -Implementations of this interface MUST copy the directive to the same field. +Implementations of this interface MUST copy the directive to the same field. The scope argument in the implementation MUST NOT be narrower than the scope in the interface, but it may be wider. ## Usage in operations -It's likely that an external schema will not use this directive. In this case, an identity MAY be chosen when writing a query. The directive does not need to be included in the query sent to the GraphQL server. +It's likely that an external schema will not use this directive. In this case, an identity MAY be chosen when writing a query. The directive SHOULD NOT be included in the query sent to the GraphQL server. ```graphql query GetAllAnimals { allAnimals { - id @apollo_client_ios_identity + id @identity string } } ``` -It is allowed for a query and a type to both use the directive to describe the same field. +It is allowed for a query and a type to both use the directive to describe the same field. If the schema and client define different scopes for the same field, the widest option is used. This allows client authors to widen the scope if required. If the server defines a field as an identity, a query SHOULD NOT choose another field. A query MUST NOT use the directive on more than one field in the same selection (unless they are in differently nested objects). -## Generated code +## Protocol conformance -If an operation contains a field marked with the `@apollo_client_ios_identity` directive (by either the schema or the operation itself), the generated SelectionSet will have a conformance to the Identifiable protocol. +If an operation contains a field marked with the `@identity` directive (by either the schema or the operation itself), the generated SelectionSet will have a conformance to the Identifiable protocol. Since the [Swift documentation](https://developer.apple.com/documentation/swift/identifiable) states that the scope and duration of an identity is unspecified, the scope parameter is ignored when deciding to add a conformance. ```swift // Inline selection @@ -77,18 +95,32 @@ The protocol requires that the identity is accessible through a public field nam public var id: String { self.uuid } ``` -## Naming conflicts +### Naming conflicts If the identity field is not called `id`, but another field called `id` is present in the selection, a custom getter cannot be added. Swift does not support using another field to handle the conformance. In this case, a conformance to Identifiable SHOULD NOT be generated. Codegen should emit a warning without stopping the generation process. +## Caching behavior + +A scope of `SELECTION` does not allow the identifier to be used as a caching key. Clients MAY use other mechanisms to determine if and how to cache the object. + +An identifier with scope `TYPE` can be combined with the `__typename` field to generate a caching key that's unique for the GraphQL Service. + +Identifiers with a scope of `SERVICE` or `GLOBAL` can be directly used as a caching key. + # Alternatives ## Automatic conformance to Identifiable The code generator could be updated to always emit a conformance to Identifiable if a scalar `id` field is present in the selection set, removing the need for a custom directive. This was suggested in Pull Request [#548](https://github.com/apollographql/apollo-ios-dev/pull/548). -## Declaring identity scope +## Apollo Kotlin's @typePolicy directive + +Apollo Kotlin has [custom directives](https://www.apollographql.com/docs/kotlin/caching/declarative-ids) that allows for client authors to specify the caching key though pure GraphQL: + +```graphql +extend type Book @typePolicy(keyFields: "id") +``` -The directive could be expanded to declare the scope of an identity (e.g. unique across the entire API or only one specific Database table). This makes the directive beneficial outside of Swift code generation, but it doesn't change the behavior of the Identifiable protocol. The [Swift documentation](https://developer.apple.com/documentation/swift/identifiable) states that the scope and duration of an identity is unspecified, so modifying the scope would not lead to a difference in the generated code. +This could be used for Identifiable conformance if a single field is provided, and caching behavior could also be matched with Apollo Kotlin. A disadvantage is that this directive is only used inside type extensions, and can't be used to mark fields directly in a query. From fcb15f03af8f61009651fcd3f9a6998eaeb41bbb Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Thu, 28 Nov 2024 23:00:51 +0100 Subject: [PATCH 4/5] Update swift-identifiable.md --- apollo-ios/Design/swift-identifiable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apollo-ios/Design/swift-identifiable.md b/apollo-ios/Design/swift-identifiable.md index d3f8e234b..93cdc7f50 100644 --- a/apollo-ios/Design/swift-identifiable.md +++ b/apollo-ios/Design/swift-identifiable.md @@ -123,4 +123,4 @@ Apollo Kotlin has [custom directives](https://www.apollographql.com/docs/kotlin/ extend type Book @typePolicy(keyFields: "id") ``` -This could be used for Identifiable conformance if a single field is provided, and caching behavior could also be matched with Apollo Kotlin. A disadvantage is that this directive is only used inside type extensions, and can't be used to mark fields directly in a query. +This could be used for Identifiable conformance by generating a getter which returns a tuple of all listed key fields, and caching behavior could also be matched with Apollo Kotlin. A disadvantage is that this directive is only used inside type extensions, and can't be used to mark fields directly in a query. From addfcfa3aaf436d0ed41789b33609f84f0682022 Mon Sep 17 00:00:00 2001 From: Lennard Sprong Date: Fri, 29 Nov 2024 07:53:06 +0100 Subject: [PATCH 5/5] Remove SELECTION scope and change overriding behavior --- apollo-ios/Design/swift-identifiable.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apollo-ios/Design/swift-identifiable.md b/apollo-ios/Design/swift-identifiable.md index 93cdc7f50..e5b214b1f 100644 --- a/apollo-ios/Design/swift-identifiable.md +++ b/apollo-ios/Design/swift-identifiable.md @@ -14,10 +14,9 @@ A concept of identity is required to allow response objects to be cached. The va The directive is defined as: ```graphql -directive @identity(scope: IdentityScope = SELECTION) on FIELD | FIELD_DEFINITION +directive @identity(scope: IdentityScope = TYPE) on FIELD | FIELD_DEFINITION enum IdentityScope { - SELECTION TYPE SERVICE GLOBAL @@ -30,10 +29,9 @@ This directive MUST be used on a field with a non-nullable scalar type. Other ty The directive can have a parameter indicating the scope of the identity. They are ordered from _narrowest_ to _widest_: -1. `SELECTION`: The identifier is only unique within the current list. Identifiers may be reused between different objects with the same typename. -2. `TYPE`: The identifier is unique for the type, e.g. an auto-incrementing column from a database table. -3. `SERVICE`: The identifier is unique across the current GraphQL Service. -4. `GLOBAL`: The identifier is unique across all GraphQL services. This can be used by identifiers generated according to [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) or [RFC 9562](https://datatracker.ietf.org/doc/html/rfc9562) (also known as Universally Unique IDentifiers or UUIDs). +1. `TYPE`: The identifier is unique for the type, e.g. an auto-incrementing column from a database table. +2. `SERVICE`: The identifier is unique across the current GraphQL Service. +3. `GLOBAL`: The identifier is unique across all GraphQL services. This can be used by identifiers generated according to [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122) or [RFC 9562](https://datatracker.ietf.org/doc/html/rfc9562) (also known as Universally Unique IDentifiers or UUIDs). ## Usage in types @@ -73,7 +71,7 @@ query GetAllAnimals { } ``` -It is allowed for a query and a type to both use the directive to describe the same field. If the schema and client define different scopes for the same field, the widest option is used. This allows client authors to widen the scope if required. +It is allowed for a query and a type to both use the directive to describe the same field. If the client and schema use different scopes for the same field, the scope defined by the schema is used. Clients SHOULD NOT define a wider scope than declared by the schema. Codegen should emit a warning in this case. If the server defines a field as an identity, a query SHOULD NOT choose another field. A query MUST NOT use the directive on more than one field in the same selection (unless they are in differently nested objects). @@ -103,7 +101,7 @@ In this case, a conformance to Identifiable SHOULD NOT be generated. Codegen sho ## Caching behavior -A scope of `SELECTION` does not allow the identifier to be used as a caching key. Clients MAY use other mechanisms to determine if and how to cache the object. +If no `@identity` directive is present, clients MAY use other mechanisms to determine if and how to cache the object. An identifier with scope `TYPE` can be combined with the `__typename` field to generate a caching key that's unique for the GraphQL Service. @@ -117,7 +115,7 @@ The code generator could be updated to always emit a conformance to Identifiable ## Apollo Kotlin's @typePolicy directive -Apollo Kotlin has [custom directives](https://www.apollographql.com/docs/kotlin/caching/declarative-ids) that allows for client authors to specify the caching key though pure GraphQL: +Apollo Kotlin has [custom directives](https://www.apollographql.com/docs/kotlin/caching/declarative-ids) that allows for client authors to specify the caching key through pure GraphQL: ```graphql extend type Book @typePolicy(keyFields: "id")