Skip to content
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

chore: add v1 advanced subscription handling rfc #1503

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
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
164 changes: 164 additions & 0 deletions rfc/advanced-subscription-handling/v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
title: Advanced Subscription Handling
author: "Jens Neuse"
---

## Problem 1: filter events based on user profile

In this case, we'd like to filter events based on the user profile.
The user provides a single profile.
An event can have multiple profiles.
We'd like to skip events that don't match the user profile.
In simple terms, skip the event if the user profile is not in the event profiles.

```graphql

directive @openfed_subscriptionMiddleware(
"""
An expression that must evaluate to a boolean.
If the expression evaluates to false, the event is skipped and not sent to the client.
If the expression evaluates to true, the event is sent to the client.
"""
includeEventBoolExpression: String
"""
An expression that must evaluate to a boolean.
If the expression evaluates to true, the event is skipped and not sent to the client.
If the expression evaluates to false, the event is sent to the client.
"""
skipEventBoolExpression: String
"""
The 'kind' argument specifies when the middleware should be executed.

'BEFORE_RESOLVE' means the middleware is executed before resolving additional fields that were not included in the event payload,
e.g. through entity representation fetches.

'AFTER_RESOLVE' means the middleware is executed after the complete Subscription response payload has been resolved.

It can be desirable to execute the middleware before resolving additional fields to avoid unnecessary work.
However, this requires the middleware to be able to make decisions based on the event payload only.
If it's not possible to make this decision based on the event payload only,
the middleware should be executed after resolving additional fields.
"""
kind: OPENFED_SUBSCRIPTION_MIDDLEWARE_KIND = BEFORE_RESOLVE
) on FIELD_DEFINITION

enum OPENFED_SUBSCRIPTION_MIDDLEWARE_KIND {
BEFORE_RESOLVE
AFTER_RESOLVE
}

directive @edfs_natsSubscribe(
subjects: [String!]!
) on FIELD_DEFINITION

type Subscription {
races(profile: String!): Race
@edfs_natsSubscribe(subjects: ["raceUpdates"])
@openfed_subscriptionMiddleware(
includeEventBoolExpression: "args.profile in this.profiles"
)
}

type Race @key(id: "id"){
id: ID!
profiles: [String!]!
}
```

Notice that we're using the `args.profile` expression to access the argument passed to the current field.
In addition, we're using `this.profiles` to access the profiles field of the current object.

## Problem 2: create subjects based on claims in a JWT

In this case, we'd like to derive the subjects from a claim in the JWT.
We're skipping directive definitions of `@openfed_subscriptionMiddleware` for brevity.

```graphql
directive @edfs_natsSubscribe(
subjects: [String!]!
"""
An expression that must evaluate to a list of strings.
"""
subjectsFromExpression: String!
) on FIELD_DEFINITION

type Subscription {
races: Race
@edfs_natsSubscribe(subjectsFromExpression: "[request.auth.claims.raceUpdates]")
@openfed_subscriptionMiddleware(
includeEventBoolExpression: "request.auth.claims.raceProfile in this.profiles"
)
}

type Race @key(id: "id"){
id: ID!
profiles: [String!]!
}
```

In this case, we're assuming that `request.auth.claims.raceUpdates` is a single string that contains the subject to get race updates.
In addition, we're assuming that `request.auth.claims.raceProfile` is a string that contains the user's race profile.
We're evaluating the `includeEventBoolExpression` to "include" the event if the `event.profile` array contains the race profile of the user,
provided in the claims of the JWT.

## Problem 3: create filters from additional internal query

In this case, we don't have the necessary information in the event payload or the JWT claims.
We need to fetch the race profile of the user from an internal service.

```graphql
# race profile service
type Query {
raceProfile(userID: ID!): String! @inaccessible
}
```

The `raceProfile` field is marked as `@inaccessible` to indicate that it's an internal field that should not be exposed to the client.

```graphql
directive @edfs_natsSubscribe(
subjects: [String!]!
"""
An expression that must evaluate to a list of strings.
"""
subjectsFromExpression: String!
) on FIELD_DEFINITION

directive @openfed_prerequisite(
"""
The query to execute to get the necessary information.
It's important that the query response shape and types match the input types, or are at least compatible.
E.g. if the input value is a non-nullable string, the query response must be a non-nullable string.
If the input value is a nullable string, the query response can be a nullable string or a non-nullable string.
If the input value is an input object with a non-nullable String field foo, the query response shape must also have a non-nullable String field foo.
"""
query: String!
variablesExpression: String!
) on ARGUMENT_DEFINITION

type Subscription {
races(
profile: String!
@openfed_prerequisite(
query: "raceProfile(userID: $userID)"
variables: "{\"userID\": request.auth.claims.sub}"
)
): Race
@edfs_natsSubscribe(subjectsFromExpression: "[request.auth.claims.raceUpdates]")
@openfed_subscriptionMiddleware(
includeEventBoolExpression: "args.profile in this.profiles"
)
}

type Race @key(id: "id"){
id: ID!
profiles: [String!]!
}
```

In this case, we're using the `@openfed_prerequisite` directive.
This directive removes the input argument `profile` from the client schema.
The Router will execute all prerequisites in the order they are defined before executing the field resolver.
The `query` argument specifies the query to execute.
The `variablesExpression` argument specifies an expression using `Expr-Lang` which must evaluate to a valid object.
As the `profile` argument is non-nullable, the `races` field will return null & an error if the prerequisite is not returning a string.
Loading