From 3083f93e0c58e2060c3a914c5ab4c85624255931 Mon Sep 17 00:00:00 2001 From: Jens Neuse Date: Thu, 9 Jan 2025 12:39:33 +0100 Subject: [PATCH] chore: add v1 advanced subscription handling rfc --- rfc/advanced-subscription-handling/v1.md | 164 +++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 rfc/advanced-subscription-handling/v1.md diff --git a/rfc/advanced-subscription-handling/v1.md b/rfc/advanced-subscription-handling/v1.md new file mode 100644 index 0000000000..85e6616864 --- /dev/null +++ b/rfc/advanced-subscription-handling/v1.md @@ -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. \ No newline at end of file