Skip to content

Design doc: add Declarative API section #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 1, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion design-docs/Expiration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ by default, unless a max age is defined on the client.
To do this, let's store both dates in the Record. Implementation: we can remove the current `Record.date` field,
and instead use the `Record.metadata` map which can store arbitrary data per field.

## API
## Programmatic API

```kotlin

Expand Down Expand Up @@ -125,3 +125,90 @@ return resolvedField
```

Note: the `maxStale` duration is to allow for a per-operation override of the max age / expiration date.

## Declarative API

### Schema directives

These directives will land in a `cache` v0.1 [Apollo Spec](https://specs.apollo.dev/).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much could we reuse the server side@cacheControl?

https://www.apollographql.com/docs/apollo-server/performance/caching/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout! We could probably reuse it with a few remarks:

  • scope is irrelevant on the client (can simply be ignored)
  • the meaning of applying the directive to a type is different to what I imagined here, but I think it may be equivalent
    • this draft: value for the type's fields
    • Apollo Server's version: value for all fields that are of the type
  • trying to wrap my head around inheritMaxAge - not sure why it's useful
  • we still need a "Field" version of the directive

Copy link
Contributor

@martinbonnin martinbonnin Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope is irrelevant on the client (can simply be ignored)

We could have cross-user cache. For an example, in Confetti, all the session data is PUBLIC, but the bookmarks are PRIVATE. We're not there yet but could be something?

Agree ignoring it for now is fine.

we still need a "Field" version of the directive

Yea, either @cacheControlField() or field definition extensions in the GraphQL 2.0 spec 🤓

Overall, agree that we could model things more precisely but the prospect of getting all of these for free from the backend is pretty enticing


```graphql
"""
Indicates that a field (or a type's fields) should be considered stale after the given duration
in seconds has passed since it has been received.

When applied on a type, all fields of the type inherit the max age.

When applied on a field whose parent type has a max age, the field's max age takes precedence.

```graphql
type User @maxAge(seconds: 10) {
id: ID!
email: String @maxAge(seconds: 20)
}
\```

`User.id` is considered stale after 10 seconds, and `User.email` after 20 seconds.
"""
directive @maxAge(seconds: Int!) on FIELD_DEFINITION | OBJECT

"""
Indicates that a field should be considered stale after the given duration in seconds has passed
since it has been received.

When applied on a field whose parent type has a max age, the field's max age takes precedence.

`@maxAgeField` is the same as `@maxAge` but can be used on type system extensions for services
that do not own the schema like client services:

```graphql
# extend the schema to set a max age on User.email.
extend type User @maxAgeField(name: "email", seconds: 20)
\```

`User.email` is considered stale after 20 seconds.
"""
directive @maxAgeField(name: String!, seconds: Int!) repeatable on OBJECT
```

### Codegen changes

#### Option A: Add max age info to `ObjectType`

```kotlin
class ObjectType internal constructor(
name: String,
keyFields: List<String>,
implements: List<InterfaceType>,
embeddedFields: List<String>,
typeMaxAge: Int?, // NEW! Contains the value of the @maxAge directive on the type (or null if not set)
fieldsMaxAge: Map<String, Int>?, // NEW! Contains the value of the @maxAge directive on the type's fields (and of @maxAgeField on the type) (or null if not set)
) : CompiledNamedType(name) {... }
```

With this option, a `CacheResolver` can access the max age information directly from the `ObjectType` that is passed to it.

Note: currently we pass only the parent type name to `CacheResolver` but we can pass the `CompiledNamedType` instead.

- Pro: the `CacheResolver` is autonomous, no need to do any 'plumbing' to pass it the generated information.
- Con: generates more fields for everybody, even users not using the feature (albeit with null values for them).

#### Option B: Generate a dedicated file for max age info

We generate a file looking like this:

```kotlin
object Expiration {
val maxAges: Map<String, Int> = mapOf(
"MyType" to 20,
"MyType.id" to 10,
// ...
)
}
```

This is the approach we took for the `Pagination` feature where we need a list of connection types.

- Pro: no codegen impact for non-users of the feature, the file can be generated only when there are fields selected that have a max age in
the schema.
- Con: more 'plumbing' - it requires to manually pass `Expiration.maxAges` to the constructor of the `CacheResolver`.