diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index ee37d2f9..4017db48 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -50,8 +50,7 @@ ** xref:subscriptions/getting-started.adoc[Getting started] ** xref:subscriptions/events.adoc[Events] ** xref:subscriptions/filtering.adoc[] -** xref:subscriptions/scaling.adoc[] -** xref:subscriptions/engines.adoc[Engines] +** xref:subscriptions/customize-cdc.adoc[] * *How-To* diff --git a/modules/ROOT/pages/directives/schema-configuration/type-configuration.adoc b/modules/ROOT/pages/directives/schema-configuration/type-configuration.adoc index 28227525..ecf8d372 100644 --- a/modules/ROOT/pages/directives/schema-configuration/type-configuration.adoc +++ b/modules/ROOT/pages/directives/schema-configuration/type-configuration.adoc @@ -118,11 +118,9 @@ enum SubscriptionFields { CREATED UPDATED DELETED - RELATIONSHIP_CREATED - RELATIONSHIP_DELETED } -directive @subscription(events: [SubscriptionFields!]! = [CREATED, UPDATED, DELETED, RELATIONSHIP_CREATED, RELATIONSHIP_DELETED]) on OBJECT | SCHEMA +directive @subscription(events: [SubscriptionFields!]! = [CREATED, UPDATED, DELETE]) on OBJECT | SCHEMA ---- === Usage diff --git a/modules/ROOT/pages/subscriptions/customize-cdc.adoc b/modules/ROOT/pages/subscriptions/customize-cdc.adoc new file mode 100644 index 00000000..e54bf6dc --- /dev/null +++ b/modules/ROOT/pages/subscriptions/customize-cdc.adoc @@ -0,0 +1,46 @@ +[[customize-cdc]] += Customize CDC subscriptions +:page-aliases: subscriptions/plugins/index.adoc, subscriptions/plugins/amqp.adoc, subscriptions/plugins/single-instance.adoc +:description: This page describes how to customize the behavior of subscriptions. + +GraphQL subscriptions to Neo4j use [Change Data Capture](https://neo4j.com/docs/cdc/current/) (CDC). +Its behavior can be configured by passing an instance of `Neo4jGraphQLSubscriptionsCDCEngine`. + +== Neo4jGraphQLSubscriptionsCDCEngine + +By default, the GraphQL library uses the same driver passed to `Neo4jGraphQL` to poll for events every second. +This behavior can be changed by creating a custom instance of `Neo4jGraphQLSubscriptionsCDCEngine`. + +The following options can be passed to the constructor: + +* `driver`: The driver to be used for CDC queries. +* `pollTime`: The interval, in milliseconds, between queries to CDC. +Defaults to 1000ms. +Note that poll time is the period between a finished request and the start of the next. +The actual time it takes for CDC events to trigger the subscription also depends on your network. +* `queryConfig`: An object with the driver query options to be passed to CDC requests. +Use the `db` field to define the target database for CDC. + +For example: + +[source, javascript, indent=0] +---- +import { Neo4jGraphQL, Neo4jGraphQLSubscriptionsCDCEngine } from '@neo4j/graphql'; + +const engine = new Neo4jGraphQLSubscriptionsCDCEngine({ + driver, + pollTime: 5000 +}) + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + subscriptions: engine, + queryConfig: { + database: "neo4j", + } + }, +}); +---- + diff --git a/modules/ROOT/pages/subscriptions/engines.adoc b/modules/ROOT/pages/subscriptions/engines.adoc deleted file mode 100644 index f4bcc18f..00000000 --- a/modules/ROOT/pages/subscriptions/engines.adoc +++ /dev/null @@ -1,221 +0,0 @@ -[[subscription-engines]] -= Subscription engines -:page-aliases: subscriptions/plugins/index.adoc, subscriptions/plugins/amqp.adoc, subscriptions/plugins/single-instance.adoc -:description: This page describes how a GraphQL subscription may be set along with a @neo4j/graphql server. - - -This page describes different ways to set up a GraphQL subscription along with a `@neo4j/graphql` server. - -== Default - -The default behavior is automatically set if the `subscriptions` feature is set to `true`, as described in xref::subscriptions/getting-started.adoc[Getting Started]: - -[source, javascript, indent=0] ----- -new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: true - }, -}); ----- - -This behavior enables a simple subscription system that works on a single instance. -It is ideal for development, testing, and servers that do not require horizontal scaling. - -[[CDC]] -== Change Data Capture label:Beta[] - -If your database supports Change Data Capture (CDC), you can use it as your mechanism for subscriptions using `Neo4jGraphQLSubscriptionsCDCEngine`. -Make sure to follow the steps described on the link:https://neo4j.com/docs/cdc/current/[CDC Documentation] to enable it for your Neo4j instance. - -Note that CDC-based subscriptions behave differently from other subscription mechanisms. -In this case, it uses the native CDC events from Neo4j database. -This has the following implications: - -* Any database change, including those changes done outside of GraphQL, will be reported. -* Relationship events are not supported at the moment. -* No additional broker mechanism is required. -All the events are received by all the instances of `@neo4j/graphql`. -* Events are not triggered immediately but are polled to the database. - -=== Usage - -`Neo4jGraphQLSubscriptionsCDCEngine` can be imported directly from the library. -The Neo4j driver is the only required argument: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL, Neo4jGraphQLSubscriptionsCDCEngine } from '@neo4j/graphql'; - -const engine = new Neo4jGraphQLSubscriptionsCDCEngine({ - driver, -}) - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: engine, - }, -}); ----- - -=== API - -The following options can be passed to the constructor: - -* `driver`: The driver to be used for CDC queries. -* `pollTime`: The interval, in milliseconds, between queries to CDC. -Defaults to 100ms. -Note that poll time is the period between one request finishing and the next one starting. -The actual time it takes for CDC events to trigger the subscription also depend on your network. -* `queryConfig`: An object with the driver query options to be passed to CDC requests. -Use the `db` field to define the target database for CDC. - -[[amqp]] -== AMQP - -Using subscriptions on a server with multiple instances can be complex, as described in xref::subscriptions/scaling.adoc[Horizontal scaling]. -Therefore, the recommended approach is to use a PubSub system, which can be achieved with an AMQP broker such as link:https://www.rabbitmq.com/[RabbitMQ]. -This is supported by the link:https://www.npmjs.com/package/@neo4j/graphql-amqp-subscriptions-engine[@neo4j/graphql-amqp-subscriptions-engine] package. - -The `@neo4j/graphql-amqp-subscriptions-engine` plugin connects to message brokers through the `AMQP 0-9-1` protocol to distribute subscription events across all server instances. - -Some brokers supporting this protocol are: - -* link:https://www.rabbitmq.com/[RabbitMQ] -* link:https://qpid.apache.org/[Apache Qpid] -* link:https://activemq.apache.org/[Apache ActiveMQ] - -The plugin can be installed with `npm`: - -[source, sh, indent=0] ----- -npm install @neo4j/graphql-amqp-subscriptions-engine ----- - -[NOTE] -==== -AMQP 1.0 is **not** supported by this plugin. -==== - -=== Usage - -The AMQP plugin should be instanced and passed to the `subscription` field in features. -This automatically enables the subscriptions with the AMQP broker as a message queue: - -[source, javascript, indent=0] ----- -const { Neo4jGraphQLAMQPSubscriptionsEngine } = require("@neo4j/graphql-amqp-subscriptions-engine"); - -const amqpSubscription = new Neo4jGraphQLAMQPSubscriptionsEngine({ - connection: { - hostname: "localhost", - username: "guest", - password: "guest", - } -}); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: amqpSubscription, - }, -}); ----- - -=== API -The following options can be passed to the constructor: - -* **connection**: an AMQP uri as a string or a configuration object. -** **hostname**: hostname to be used. -Defaults to `localhost`. -** **username**: defaults to `guest`. -** **password**: defaults to `guest`. -** **port**: port of the AMQP broker. -Defaults to `5672`. -* **exchange**: the exchange to be used in the broker. -Defaults to `neo4j.graphql.subscriptions.fx`. -* **version**: the AMQP version to be used. -Currently only `0-9-1` is supported. - -Additionally, any option supported by link:https://www.npmjs.com/package/amqplib[amqplib] can be passed to `connection`. -To set these configurations up, use the following method: - -* **close(): Promise**: Closes the connection and channel created, and unbinds the event emitter. - -[[custom-subscription]] -== Custom subscription engine - -If none of the existing engines is valid for your use case, you can create a new engine to connect to any broker you may need. -For that, you need to create a new class defining your messaging behavior and it must contain: - -* An `EventEmitter` property called `events` that should emit an event every time the broker sends a message. -* A `publish` method that should publish a new event to the broker. -* Optionally, an `init` method returning a promise that should be called on `getSchema`. -This is useful for setting up the connection to a broker. - -In case you want to handle subscriptions using link:https://redis.io/[redis]: - -[source, javascript, indent=0] ----- -// Note: This is an example of a custom subscription behavior and not a production ready redis implementation. -class CustomRedisSubscriptionEngine { - constructor(redisClient) { - this.client = redisClient; - this.events = new EventEmitter(); - } - - // This method connects to Redis and sends messages to the eventEmitter when receiving events. - async init(){ - await this.client.connect(); - this.subscriber = this.client.duplicate() - this.publisher = this.client.duplicate(); - await this.subscriber.connect(); - await this.publisher.connect(); - - await this.subscriber.subscribe("graphql-subscriptions", (message) => { - const eventMeta = JSON.parse(message); - this.events.emit(eventMeta.event, eventMeta); // Emits a new event when receiving a new message from redis - }); - } - - async publish(eventMeta) { - await this.publisher.publish("graphql-subscriptions", JSON.stringify(eventMeta)); // Sends a message to redis - } -} - -const client = createClient(); // From https://www.npmjs.com/package/redis -const redisSubscriptions = new CustomRedisSubscriptionEngine(client) - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - subscriptions: redisSubscriptions, - }, -}); ----- - -Note that extra properties and methods are often needed to handle the connection to the broker. -However, as long as the messages are sent to the broker in the `publish` method and that these messages are received and then emitted through the `events` property, the subscriptions are properly handled. - -=== Using Typescript - -If using Typescript, you may import the interface `Neo4jGraphQLSubscriptionsEngine` to implement your own class. -Ensure the API is correctly defined: - -[source, typescript] ----- -class CustomRedisEngine implements Neo4jGraphQLSubscriptionsEngine {} ----- - -[NOTE] -==== -Events are sent in order to the class. -However, order is not guaranteed once these events have been broadcasted through a broker. -For cases when ordering is important, you must set up the field `timestamp` in the subscriptions payload. -==== diff --git a/modules/ROOT/pages/subscriptions/events.adoc b/modules/ROOT/pages/subscriptions/events.adoc index de284539..2e1ec440 100644 --- a/modules/ROOT/pages/subscriptions/events.adoc +++ b/modules/ROOT/pages/subscriptions/events.adoc @@ -7,12 +7,6 @@ subscriptions/events/update.adoc This page covers a variety of subscription options offered by the Neo4j GraphQL Library. -[NOTE] -==== -Only changes made through `@neo4j/graphql` should trigger the events here described. -Changes made directly to the database or using the xref::directives/custom-logic.adoc#_cypher[`@cypher` directive] will **not** trigger any event. -==== - == `CREATE` Subscriptions to `CREATE` events listen *only* to newly created nodes, not new relationships. @@ -127,1702 +121,3 @@ subscription { } } ---- - -[[create_relationship]] -== `CREATE_RELATIONSHIP` - -Subscriptions to `CREATE_RELATIONSHIP` events listen to new relationships being created and contain information about the connected nodes. -These events: - -* Are only available for types that define relationship fields. -* Contain relationship-specific information, such as the relationship field name and the object containing all relationship field names of the specified type. -* Trigger an equivalent number of events compared to the relationships created, in case a new relationship is created following a mutation and the type targeted is responsible for defining two or more relationships in the schema. -* Contain the relationships object populated with the newly created relationship properties for one single relationship name only (all other relationship names should have a null value). -* Contain the properties of the nodes connected through the relationship, as well as the properties of the new relationship, if any. - -[NOTE] -==== -Connected nodes that may or may not have previously existed are not covered by this subscription. -To subscribe to these nodes' updates, use the xref:subscriptions/events.adoc#_create[`CREATE`] or the xref:subscriptions/events.adoc#_update[`UPDATE`] subscription. -==== - -Subscriptions to `CREATE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipCreated`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `CREATE_RELATIONSHIP`). -* `timestamp`: the timestamp in which the mutation was made. -If a same query triggers multiple events, they should have the same timestamp. -* ``: top-level properties of the targeted nodes, without relationships, before the `CREATE_RELATIONSHIP` operation was triggered. -* `relationshipFieldName`: the field name of the newly created relationship. -* `createdRelationship`: an object having all field names of the nodes affected by the newly created relationships. -While any event unrelated to `relationshipFieldName` should be `null`, the ones which are related should contain the relationship properties, if defined, and a `node` key containing the properties of the node on the other side of the relationship. -Only top-level properties, without relationships, are available and they are the properties that already existed before the `CREATE_RELATIONSHIP` operation took place. - -[NOTE] -==== -Irrespective of the relationship direction in the database, the `CREATE_RELATIONSHIP` event is bound to the type targeted for the subscription. -Consequently, if types A and B have xref:subscriptions/events.adoc#_non_reciprocal_relationships[non-reciprocal relationships] and a GraphQL operation creates a relationship between them (despite being already previously connected in the database), the `CREATE_RELATIONSHIP` event should only return the subscription to the type A. -==== - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") -} - -type Actor @node { - name: String -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Reviewer @node { - name: String - reputation: Int -} - -type Reviewed @relationshipProperties { - score: Int! -} ----- - -Now consider a mutation creating an `Actor` named `Tom Hardy` and a `Reviewer` named `Jane` is connected through a relationship to a `Movie` titled `Inception`. -A `CREATE_RELATIONSHIP` subscription in this case should receive the following events: - -[source,graphql,indent=0] ----- -{ - # 1 - relationship type `ACTED_IN` - event: "CREATE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object - createdRelationship: { - actors: { - screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` - node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type - name: "Tom Hardy" - } - }, - reviewers: null # relationship declared by this field name is not covered by this event, check out the following... - } -} -{ - # 2 - relationship type `REVIEWED` - event: "CREATE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name - createdRelationship: { - actors: null, # relationship declared by this field name is not covered by this event - reviewers: { # field name equal to `relationshipFieldName` - score: 8, - node: { - name: "Jane", - reputation: 9 - } - } - } -} ----- - -=== Standard types - -For another example, this time creating a relationship with standard types, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Actor @node { - name: String -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} ----- - -A subscription to any `Movie` with newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -=== Abstract types - -When using abstract types with relationships, you need to specify one or more of the corresponding concrete types when performing the subscription operation. - -These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a concrete type. - -As an example, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -union Director = Person | Actor - -type Actor @node { - name: String -} - -type Person @node { - name: String - reputation: Int -} - -type Directed @relationshipProperties { - year: Int! -} ----- - -A subscription to any `Movie` newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - directors { - year - node { - ... on PersonEventPayload { # generated type - name - reputation - } - ... on ActorEventPayload { # generated type - name - } - } - } - } - } -} ----- - -=== Interface - -For an example in which a relationship is created with an interface, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) -} - -interface Reviewer { - reputation: Int! -} - -type Magazine implements Reviewer @node { - title: String - reputation: Int! -} - -type Influencer implements Reviewer @node { - name: String - reputation: Int! -} - -type Review @relationshipProperties { - score: Int! -} ----- - -A subscription to any `Movie` newly created relationships should look like this: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - reviewers { - score - node { - reputation - ... on MagazineEventPayload { # generated type - title - reputation - } - ... on InfluencerEventPayload { # generated type - name - reputation - } - } - } - } - } -} ----- - -=== Non-reciprocal relationships - -Non-reciprocal relationships can be described, for example, as when a type A and a type B hold a relationship, but, in the GraphQL schema, type A is the one defining the relationship to B, while B does not define a relationship to A. - -To illustrate that, consider the following type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -type Actor @node { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) -} - -type Person @node { - name: String - reputation: Int -} - -union Director = Person | Actor - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Directed @relationshipProperties { - year: Int! -} ----- - -Note that the type definitions contain two relationships: - -* `ACTED_IN`, which has a corresponding field defined in both the `Movie` and `Actor` types and, as such, can be considered a reciprocal relationship. -* `DIRECTED`, which is only defined in the `Movie` type. -The `Director` type does not define a matching field and, as such, it can be considered a non-reciprocal relationship. - -Considering the three types previously described (`Movie`, `Actor`, and `Person`), subscribing to `CREATE_RELATIONSHIP` is *not* possible only in the case of the `Person` type, for it does not define any relationships. -For the other two types, here is how to subscribe: - -.`Movie` type -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - actors { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - name - } - } - directors { # corresponds to the `DIRECTED` relationship type - year - node { - ... on PersonEventPayload { - name - reputation - } - ... on ActorEventPayload { - name - } - } - } - } - } -} ----- - -.`Actor` type -[source,graphql,indent=0] ----- -subscription { - actorRelationshipCreated { - event - timestamp - actor { - name - } - relationshipFieldName - createdRelationship { - movies { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - title - genre - } - } - # no other field corresponding to the `DIRECTED` relationship type - } - } -} ----- - -The presence of the `Movie` field inside of `createdRelationship` for the `actorRelationshipCreated` subscription reflects the fact that the `ACTED_IN`-typed relationship is reciprocal. - -Therefore, when a new relationship of this type is created, such as by running this mutation: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - actors: { - create: [ - { - node: { - name: "Keanu Reeves" - }, - edge: { - screenTime: 420 - } - } - ] - }, - title: "John Wick", - genre: "Action" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on both types: - -[source,graphql,indent=0] ----- -{ - # from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "John Wick", - genre: "Action" - } - relationshipFieldName: "actors", - createdRelationship { - actors: { - screenTime: 420, - node: { - name: "Keanu Reeves" - } - }, - directors: null - } -}, -{ - # from `actorRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Keanu Reeves" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - screenTime: 420, - node: { - title: "John Wick", - genre: "Action" - } - } - } -} ----- - -Now, since the `DIRECTED` relationship between types `Movie` and `Director` is *not* reciprocal, executing this mutation: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - directors: { - Actor: { # relationship 1 - create: [ - { - node: { - name: "Woody Allen" - }, - edge: { - year: 1989 - } - } - ] - }, - Person: { # relationship 2 - create: [ - { - node: { - name: "Francis Ford Coppola", - reputation: 100 - }, - edge: { - year: 1989 - } - } - ] - } - }, - title: "New York Stories", - genre: "Comedy" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on the `Movie` type: - -[source,graphql,indent=0] ----- -{ - # relationship 1 - from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - createdRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Woody Allen" - } - } - } -}, -{ - # relationship 2 - from `movieRelationshipCreated` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - createdRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Francis Ford Coppola", - reputation: 100 - } - } - } -} ----- - -=== Types using the same Neo4j label - -One scenario to be considered is when Neo4j labels are overriden by a specific GraphQL type. -This can be achieved using the `@node` directive, by specifying the `label` argument. -However, in the majority of cases, this is *not* the recommended approach to design your API. - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Actor @node(label: "Person") { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -type Person @node { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -type Movie @node { - title: String - genre: String - people: [Person!]! @relationship(type: "PART_OF", direction: IN) - actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) -} ----- - -Although the example features 3 GraphQL types, in Neo4j there should only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. - -At the database level there is no distinction between `Actor` and `Person`. -Therefore, when creating a new relationship of type `PART_OF`, there should be one event for each of the 2 types. - -Considering the following subscriptions: - -[source,graphql,indent=0] ----- -subscription { - movieRelationshipCreated { - event - timestamp - movie { - title - genre - } - relationshipFieldName - createdRelationship { - people { # corresponds to the `PART_OF` relationship type - node { - name - } - } - actors { # corresponds to the `PART_OF` relationship type - node { - name - } - } - } - } -} - -subscription { - actorRelationshipCreated { - event - timestamp - actor { - name - } - relationshipFieldName - createdRelationship { - movies { # corresponds to the `PART_OF` relationship type - node { - title - genre - } - } - } - } -} ----- - -Running a mutation such as: - -[source,graphql,indent=0] ----- -mutation { - createMovies( - input: [ - { - people: { # relationship 1 - create: [ - { - node: { - name: "John Logan" - } - } - ] - }, - actors: { # relationship 2 - create: [ - { - node: { - name: "Johnny Depp" - } - } - ] - }, - title: "Sweeney Todd", - genre: "Horror" - } - ] - ) { - movies { - title - genre - } - } -} ----- - -Should result in this: - -[source,graphql,indent=0] ----- -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Person` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - createdRelationship { - people: { - node: { - name: "John Logan" - } - }, - actors: null - } -}, -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Actor` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - createdRelationship { - people: null, - actors: { - node: { - name: "John Logan" - } - } - } -}, -{ - # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`,`Person` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - createdRelationship { - people: { - node: { - name: "Johnny Depp" - } - }, - actors: null - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` - event: "CREATE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - createdRelationship { - people: null, - actors: { - node: { - name: "Johnny Depp" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -In case you have subscribed to `Person` as well, you should receive two more events: - -[source,graphql,indent=0] ----- -{ - # relationship 1 `movies` - for GraphQL types `Person`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Person`, `Movie` - event: "CREATE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - createdRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -== `DELETE_RELATIONSHIP` - -Subscriptions to `DELETE_RELATIONSHIP` events listen to relationships being deleted and contain information about the previously connected nodes of a specified type. -This kind of subscription: - -* Is only available for types that define relationship fields. -* Contains relationship-specific information, such as the relationship field name and the object containing all relationship field names of the specified type. -This object should be populated with properties according to the deleted relationship. -* Triggers an equivalent number of events compared to relationships deleted, in case a relationship is deleted following a mutation and the type targeted is responsible for defining two or more relationships in the schema. -* Contains the relationships object populated with the newly deleted relationship properties for one single relationship name only (all other relationship names should have a null value). -* Contains the properties of the nodes connected through the relationship, as well as the properties of the newly deleted relationship, if any. - -[NOTE] -==== -Disconnected nodes that may or may not have been deleted in the process are not covered by this subscription. -To subscribe to these nodes' updates, use the `DELETE` subscriptions. -==== - -Subscriptions to `DELETE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipDeleted`, which contains the following fields: - -* `event`: the event triggering this subscription (in this case, `DELETE_RELATIONSHIP`). -* `timestamp`: the timestamp in which the mutation was made. If a same query triggers multiple events, they should have the same timestamp. -* ``: top-level properties of the targeted nodes, without relationships, before the `DELETE_RELATIONSHIP` operation was triggered. -* `relationshipFieldName`: the field name of the newly deleted relationship. -* `deletedRelationship`: an object having all field names of the nodes affected by the newly deleted relationships. -While any event unrelated to `relationshipFieldName` should be `null`, the ones which are related should contain the relationship properties, if defined, and a node key containing the properties of the node on the other side of the relationship. -Only top-level properties, without relationships, are available and they are the properties that already existed before the `DELETE_RELATIONSHIP` operation took place. - -[NOTE] -==== -Irrespective of the relationship direction in the database, the `DELETE_RELATIONSHIP` event is bound to the type targeted for the subscription. -Consequently, if types A and B have xref:subscriptions/events.adoc#delete-non-reciprocal-relationships[non-reciprocal relationships] and a GraphQL operation deletes a relationship between them (despite being already previously disconnected in the database), the `DELETE_RELATIONSHIP` event should only return the subscription to the type A. -==== - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - reviewers: [Reviewer] @relationship(type: "REVIEWED", direction: IN, properties: "Reviewed") -} - -type Actor @node { - name: String -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Reviewer @node { - name: String - reputation: Int -} - -type Reviewed @relationshipProperties { - score: Int! -} ----- - -Now consider a mutation deleting the `Actor` named `Tom Hardy` and the `Reviewer` named `Jane`, which are connected through a relationship to a `Movie` titled `Inception`. -A `DELETE_RELATIONSHIP` subscription in this case should receive the following events: - -[source,graphql,indent=0] ----- -{ - # 1 - relationship type `ACTED_IN` - event: "DELETE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "actors", # notice the field name specified here is populated below in the `createdRelationship` object - deletedRelationship: { - actors: { - screenTime: 1000, # relationship properties for the relationship type `ACTED_IN` that was deleted - node: { # top-level properties of the node at the other end of the relationship, in this case `Actor` type, before the delete occurred - name: "Tom Hardy" - } - }, - reviewers: null # relationship declared by this field name is not covered by this event, check out the following... - } -} -{ - # 2 - relationship type `REVIEWED` - event: "DELETE_RELATIONSHIP", - timestamp, - movie: { - title: "Inception", - genre: "Adventure" - }, - relationshipFieldName: "reviewers", # this event covers the relationship declared by this field name - deletedRelationship: { - actors: null, # relationship declared by this field name is not covered by this event - reviewers: { # field name equal to `relationshipFieldName` - score: 8, - node: { - name: "Jane", - reputation: 9 - } - } - } -} ----- - -=== Standard types - -As an example, consider these type definitions: - -[source,graphql,indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") -} - -type Actor @node { - name: String -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} ----- - -A subscription to any `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -==== Delete Relationship with Abstract Types - -When using Abstract Types with relationships, you will need to specify one or more of the corresponding Concrete Types when performing the subscription operation. - -These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a **Concrete Type**. - -===== Union Example -Considering the following type definitions: - -[source, graphql] ----- -type Movie @node { - title: String - genre: String - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -union Director = Person | Actor - -type Actor @node { - name: String -} - -type Person @node { - name: String - reputation: Int -} - -type Directed @relationshipProperties { - year: Int! -} ----- - -A subscription to `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - directors { - year - node { - ... on PersonEventPayload { # generated type - name - reputation - } - ... on ActorEventPayload { # generated type - name - } - } - } - } - } -} ----- - -===== Interface Example -Considering the following type definitions: - -[source, graphql] ----- -type Movie @node { - title: String - genre: String - reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) -} - -interface Reviewer { - reputation: Int! -} - -type Magazine implements Reviewer @node { - title: String - reputation: Int! -} - -type Influencer implements Reviewer @node { - name: String - reputation: Int! -} - -type Review @relationshipProperties { - score: Int! -} ----- - -A subscription to `Movie` deleted relationships would look like: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - reviewers { - score - node { - reputation - ... on MagazineEventPayload { # generated type - title - reputation - } - ... on InfluencerEventPayload { # generated type - name - reputation - } - } - } - } - } -} ----- - -[[delete-non-reciprocal-relationships]] -==== Non-reciprocal relationships - -Considering the following type definitions: - -[source, graphql] ----- -type Movie @node { - title: String - genre: String - actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) -} - -type Actor @node { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) -} - -type Person @node { - name: String - reputation: Int -} - -union Director = Person | Actor - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Directed @relationshipProperties { - year: Int! -} ----- - -The type definitions contain 2 relationships: types `ACTED_IN` and `DIRECTED`. - -It can be observed that the `ACTED_IN` relationship has a corresponding field defined in both the `Movie` and `Actor` types. As such, we can say that `ACTED_IN` is a reciprocal relationship. - -`DIRECTED` on the other hand is only defined in the `Movie` type. The `Director` type does not define a matching field. As such, we can say `DIRECTED` is **not** a reciprocal relationship. - -Let us now take a look at how we can subscribe to deleted relationships for the 3 types defined above: - -===== Movie - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - actors { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - name - } - } - directors { # corresponds to the `DIRECTED` relationship type - year - node { - ... on PersonEventPayload { - name - reputation - } - ... on ActorEventPayload { - name - } - } - } - } - } -} ----- - -===== Person - -As the `Person` type does not define any relationships, it is **not** possible to subscribe to `DELETE_RELATIONSHIP` events for this type. - -===== Actor - -[source, graphql] ----- -subscription { - actorRelationshipDeleted { - event - timestamp - actor { - name - } - relationshipFieldName - deletedRelationship { - movies { # corresponds to the `ACTED_IN` relationship type - screenTime - node { - title - genre - } - } - # no other field corresponding to the `DIRECTED` relationship type - } - } -} ----- - -The presence of the `movie` field inside of `deletedRelationship` for the `actorRelationshipDeleted` subscription reflects the fact that the `ACTED_IN` typed relationship is reciprocal. - -Therefore, when a relationship of this type is deleted, such as by running the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - actors: { - create: [ - { - node: { - name: "Keanu Reeves" - }, - edge: { - screenTime: 420 - } - } - ] - }, - title: "John Wick", - genre: "Action" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title_EQ: "John Wick" - } - ) { - nodesDeleted - } -} ----- - -Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on both types): - -[source, graphql] ----- -{ - # from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "John Wick", - genre: "Action" - } - relationshipFieldName: "actors", - deletedRelationship { - actors: { - screenTime: 420, - node: { - name: "Keanu Reeves" - } - }, - directors: null - } -}, -{ - # from `actorRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Keanu Reeves" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - screenTime: 420, - node: { - title: "John Wick", - genre: "Action" - } - } - } -} ----- - -Since the `DIRECTED` relationship between types `Movie` and `Director` is **not** reciprocal, executing the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - directors: { - Actor: { # relationship 1 - create: [ - { - node: { - name: "Woody Allen" - }, - edge: { - year: 1989 - } - } - ] - }, - Person: { # relationship 2 - create: [ - { - node: { - name: "Francis Ford Coppola", - reputation: 100 - }, - edge: { - year: 1989 - } - } - ] - } - }, - title: "New York Stories", - genre: "Comedy" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title_EQ: "New York Stories" - } - ) { - nodesDeleted - } -} ----- - -Two events will be published (given that we subscribed to `DELETE_RELATIONSHIP` events on the `Movie` type): - -[source, graphql] ----- -{ - # relationship 1 - from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - deletedRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Woody Allen" - } - } - } -}, -{ - # relationship 2 - from `movieRelationshipDeleted` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "New York Stories", - genre: "Comedy" - } - relationshipFieldName: "directors", - deletedRelationship { - actors: null, - directors: { - year: 1989, - node: { - name: "Francis Ford Coppola", - reputation: 100 - } - } - } -} ----- - -=== Special Considerations - -==== Types using the same Neo4j label - -One case that deserves special consideration is overriding the label in Neo4j for a specific GraphQL type. -This can be achieved using the `@node` directive, by specifying the `label` argument. - -NOTE: While this section serves an informative purpose, it should be mentioned that, in the majority of cases, this is not the recommended approach of designing your API. - -Consider the following type definitions: - -[source, graphql] ----- -type Actor @node(label: "Person") { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -typePerson @node { - name: String - movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT) -} - -type Movie @node { - title: String - genre: String - people: [Person!]! @relationship(type: "PART_OF", direction: IN) - actors: [Actor!]! @relationship(type: "PART_OF", direction: IN) -} ----- - -Although we have 3 GraphQL types, in Neo4j there will only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. - -At the database level there is no distinction between `Actor` and `Person`. Therefore, when deleting a relationship of type `PART_OF`, there will be one event for each of the 2 types. - -Considering the following subscriptions: - -[source, graphql] ----- -subscription { - movieRelationshipDeleted { - event - timestamp - movie { - title - genre - } - relationshipFieldName - deletedRelationship { - people { # corresponds to the `PART_OF` relationship type - node { - name - } - } - actors { # corresponds to the `PART_OF` relationship type - node { - name - } - } - } - } -} - -subscription { - actorRelationshipDeleted { - event - timestamp - actor { - name - } - relationshipFieldName - deletedRelationship { - movies { # corresponds to the `PART_OF` relationship type - node { - title - genre - } - } - } - } -} ----- - -Running the following mutations: - -[source, graphql] ----- -mutation { - createMovies( - input: [ - { - people: { # relationship 1 - create: [ - { - node: { - name: "John Logan" - } - } - ] - }, - actors: { # relationship 2 - create: [ - { - node: { - name: "Johnny Depp" - } - } - ] - }, - title: "Sweeney Todd", - genre: "Horror" - } - ] - ) { - movies { - title - genre - } - } -} - -mutation { - deleteMovies( - where: { - title_EQ: "Sweeney Todd" - } - ) { - nodesDeleted - } -} ----- - -Result in the following events: - -[source, graphql] ----- -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Person` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - deletedRelationship { - people: { - node: { - name: "John Logan" - } - }, - actors: null - } -}, -{ - # relationship 1 `people` - for GraphQL types `Movie`, `Actor` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - deletedRelationship { - people: null, - actors: { - node: { - name: "John Logan" - } - } - } -}, -{ - # relationship 1 `movies` - for GraphQL types `Actor`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`,`Person` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "people", - deletedRelationship { - people: { - node: { - name: "Johnny Depp" - } - }, - actors: null - } -}, -{ - # relationship 2 `actors` - for GraphQL types `Movie`, `Actor` - event: "DELETE_RELATIONSHIP" - timestamp - movie { - title: "Sweeney Todd", - genre: "Horror" - } - relationshipFieldName: "actors", - deletedRelationship { - people: null, - actors: { - node: { - name: "Johnny Depp" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Actor`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- - -Had we subscribed to `Person` as well, we would have received two more events: - -[source, graphql] ----- -{ - # relationship 1 `movies` - for GraphQL types `Person`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "John Logan" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, -{ - # relationship 2 `movies` - for GraphQL types `Person`, `Movie` - event: "DELETE_RELATIONSHIP" - timestamp - actor { - name: "Johnny Depp" - } - relationshipFieldName: "movies", - deletedRelationship { - movies: { - node: { - title: "Sweeney Todd", - genre: "Horror" - } - } - } -}, ----- \ No newline at end of file diff --git a/modules/ROOT/pages/subscriptions/filtering.adoc b/modules/ROOT/pages/subscriptions/filtering.adoc index 540f0f81..610ec5f8 100644 --- a/modules/ROOT/pages/subscriptions/filtering.adoc +++ b/modules/ROOT/pages/subscriptions/filtering.adoc @@ -211,544 +211,3 @@ subscription { } } ---- - - -== Subscribing to relationship events - -When subscribing to relationship events, the `where` argument still allows specifying filters on the top-level properties of the targeted nodes. -It also supports specifying filters on the relationship properties (`edge`) and on the top-level properties (`node`) of the nodes at the other end of the relationship. -This is done by using the operators previously described, and the usage is very similar to xref:subscriptions/filtering.adoc#node-events-usage[subscribing to node events]. - -However, filtering by relationship events is an even more powerful logic. -This is because these filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship, provided that they are connected to abstract types. - -Note that each relationship field specified is combined with the others using a xref:subscriptions/filtering.adoc#filter-logical-or[logical `OR`]. -Only events matching these relationship field names are returned in the subscription. - -You can further filter each relationship field by node and relationship properties. -These fields are combined in the resulting filter with a xref:subscriptions/filtering.adoc#filter-logical-and[logical `AND`]. - -As an example, in the following type definitions: - -[source, graphql, indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Actor @node { - name: String -} ----- - -The format of the `where` argument is: - -[source, graphql, indent=0] ----- -{ - movie: { - # top-level properties of the node targeted for the subscription operation, supports operators - title_IN: ["The Matrix", "Fight Club"] - }, - createdRelationship: { - actors: { # field name corresponding to a relationship in the type definition of the node targeted for the subscription operation - edge: { - # properties of the relationship, supports operators - screenTime_GT: 10, - }, - node: { - # top-level properties of the node on the other end of the relationship, supports operators - name_STARTS_WITH: "Brad" - } - } - } -} ----- - -The following sections feature examples of how filtering can be applied when creating a subscription to relationship events. - -=== Newly created relationship - -The following example filters the subscriptions to newly created relationships that are connecting a `Movie` from genres other than "Drama", and to an `Actor` with a screen time bigger than 10 minutes: - -[source, graphql, indent=0] ----- -subscription { - movieRelationshipCreated(where: { movie: { NOT: { genre_EQ: "Drama" } }, createdRelationship: { actors: { edge: { screenTime_GT: 10 } } } }) { - movie { - title - } - createdRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -[NOTE] -==== -The `where` argument only filters already existing properties at the moment of the relationship creation. -==== - -=== Newly deleted relationship - -The following example filters the subscriptions to deleted relationships that were connecting a `Movie` of the genre `"Comedy"` or `"Adventure"` to an `Actor` named `"Jim Carrey"`: - -[source, graphql, indent=0] ----- -subscription { - movieRelationshipDeleted(where: { movie: { genre_IN: ["Comedy", "Adventure"] }, createdRelationship: { actors: { node: { name_EQ: "Jim Carrey" } } } }) { - movie { - title - } - deletedRelationship { - actors { - screenTime - node { - name - } - } - } - } -} ----- - -[NOTE] -==== -The `where` argument only filters properties that already existed before the relationship deletion. -==== - -=== Relationship-related filters - -In addition to filtering node or relationship properties, the relationship-related filtering logic is even more powerful. -This is because these filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship, provided that they are connected to abstract types. - -The following examples are valid for both `CREATE_RELATIONSHIP` and `DELETE_RELATIONSHIP` events. -Their purpose is to illustrate the various ways in which a subscription to a relationship event can be filtered. - -Considering the following type definitions: - -[source, graphql, indent=0] ----- -type Movie @node { - title: String - genre: String - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN) - reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN) -} - -type ActedIn @relationshipProperties { - screenTime: Int! -} - -type Actor @node { - name: String -} - -type Person implements Reviewer @node { - name: String - reputation: Int -} - -union Director = Person | Actor - -type Directed @relationshipProperties { - year: Int! -} - -interface Reviewer { - reputation: Int! -} - -type Magazine implements Reviewer @node { - title: String - reputation: Int! -} - -type Review @relationshipProperties @node { - score: Int! -} ----- - -And the base subscription operation: - -[source, graphql, indent=0] ----- -subscription MovieRelationshipDeleted($where: MovieRelationshipDeletedSubscriptionWhere) { - movieRelationshipDeleted(where: $where) { - movie { - title - } - deletedRelationship { - actors { - screenTime - node { - name - } - } - directors { - year - node { - ... on PersonEventPayload { # generated type - name - reputation - } - ... on ActorEventPayload { # generated type - name - } - } - } - reviewers { - score - node { - reputation - ... on MagazineEventPayload { # generated type - title - reputation - } - ... on PersonEventPayload { # generated type - name - reputation - } - } - } - } - } -} ----- - -You can use the following `where` inputs in the GraphQL variable values to get different results: - -==== Filtering via implicit/explicit declaration - -Implicit or explicit declaration is used to filter specific relationship types that are expected to be returned to a subscription. - -For example, when subscribing to created or deleted relationships to a `Movie`, a user might only be interested in the relationship of type `ACTED_IN`, but indifferent to the properties of the `Actor` node or the other relationships connected to it. -In this case, the corresponding field name of this relationship is `actors`. - -By explicitly specifying the `actors` field name, you can filter-out events to other relationship properties: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: {} # no properties specified here, therefore all relationships to this field name will be returned - } - } -} ----- - -In case you are interested in `Actor` nodes conforming to some filters, for example with the name starting with the letter "A", the procedure is no different than xref:subscriptions/filtering.adoc#node-events-usage[subscribing to node events]: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: { - node: { # use operations to specify filers on the top-level properties of the node at the other end of the relationship - name_STARTS_WITH: "A" - } - } - } - } -} ----- - -If you are also interested in the relationship itself conforming to some filters, such as the `Actor` having spent no more than 40 minutes in the `Movie`, this is how the query may look: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: { - edge: { # use operations to specify filers on the top-level properties of the relationship - screenTime_LT: 40, - } - node: { - name_EQ: "Alvin" - } - } - } - } -} ----- - -Multiple relationship types can also be included in the returned subscriptions by explicitly specifying the corresponding field names. -For instance: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: {}, # include all relationships corresponding of type `ACTED_IN` - directors: {} # include all relationships corresponding of type `DIRECTED` - # exclude relationships of type `REVIEWED` - } - } -} ----- - -Now, if you are interested in all relationship types, you can either express this implicitly by not specifying any: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: {} # include all relationships of all types - } -} ----- - -Or explicitly by specifying the field names of all the relationships connected to the type targeted for the subscription: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - # include all relationships of all types - # subscription target type is `Movie`, which has the following relationship field names: - actors: {}, - directors: {}, - reviewers: {} - } - } -} ----- - -Note, however, that as **any** filters are applied to **any** of the relationships, explicitly including those that you are interested in subscribing to is a **mandatory** step. - -For example, if all relationships should be returned, but you want to filter-out the `REVIEWED` ones which have a score lower than 7, this is how your query may look like: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: {}, # include all relationships of type `ACTED_IN` - directors: {}, # include all relationships of type `DIRECTED` - reviewers: { # include all relationships of type `REVIEWED`, with the score property greater than 7 - edge: { - score_GT: 7 - } - } - } - } -} ----- - -Different filters can also be applied to different relationships without any constraints. -For example: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - actors: { # include some relationships of type `ACTED_IN`, filtered by relationship property `screenTime` and node property `name` - edge: { - screenTime_LT: 60, - }, - node: { - name_IN: ["Tom Hardy", "George Clooney"] - } - }, - directors: {}, # include all relationships of type `DIRECTED` - reviewers: { # include some relationships of type `REVIEWED`, filtered by relationship property `score` only - edge: { - score_GT: 7 - } - } - } - } -} ----- - -[[filter-logical-or]] - -[NOTE] -==== -In the previous example, there is an implicit logical `OR` between the `actors`, `directors`, and `reviewers` relationship fields. -This is to say that a relationship of **either** type `ACTED_IN` **or** of type `DIRECTED` **or** of type `REVIEWED` should trigger the previously described subscription. -==== - -[[filter-logical-and]] -[NOTE] -==== -There is an implicit logical `AND` between the `edge` and `node` fields inside of the `actors` relationship field. -In other words, the relationship of type `ACTED_IN` with the property `screenTime` less than 60 **and** a target node with name in `["Tom Hardy", "George Clooney"]` should trigger the subscription. -==== - -=== Abstract types - -The following sections describe how to filter subscriptions using abstract types. - -==== Union type - -This example illustrates how to filter the node at the other end of the relationship when it is of a union type: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: { # relationship to a union type - Person: { # concrete type that makes up the union type - edge: { - year_GT: 2010 - }, - node: { - name_EQ: "John Doe", - reputation_EQ: 10 - } - }, - Actor: { # concrete type that makes up the union type - edge: { - year_LT: 2005 - }, - node: { - name_EQ: "Tom Hardy" - } - } - }, - } - } -} ----- - -The result is that only relationships of type `DIRECTED` are returned to the subscription, where the `Director` is a `Person` named `"John Doe"`, who directed the movie after 2010, **or** where the `Director` is an `Actor` named `"Tom Hardy"` who directed the movie before 2005. - -Note that the relationship field name is split into multiple sections, one for each of the concrete types that make up the union type. -The relationship properties do not exist outside the confines of one of these sections, even though the properties are the same. - -Now, take the other example that did not explicitly specify the concrete types: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: {}, # include all relationships of type `DIRECTED` - } - } -} ----- - -Following the same logic as for the relationship field names: when nothing is explicitly provided, then all is accepted. -Thus relationships of type `DIRECTED`, established between a `Movie` and any of the concrete types that make up the union type `Director` are returned to the subscription. - -It is equivalent to the following: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: { # include all relationships of type `DIRECTED` - Actor: {}, - Person: {} - } - } - } -} ----- - -Note that explicitly specifying a concrete type excludes the others from the returned events: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: { - Actor: {} # include all relationships of type `DIRECTED` to an `Actor` type - } - } - } -} ----- - -In this case, only relationships of type `DIRECTED` between a `Movie` and an `Actor` are returned to the subscription. -Those between a `Movie` and a `Person` are filtered out. - -One reason why this might be done is to include some filters on the `Actor` type: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: { - Actor: { # include some relationships of type `DIRECTED` to an `Actor` type, that conform to the filters - node: { - NOT: { name_EQ: "Tom Hardy" } - } - } - } - } - } -} ----- - -To include filters on the `Actor` type, but also include the `Person` type in the result, you need to make the intent explicit: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - directors: { - Actor: { # include some relationships of type `DIRECTED` to an `Actor` type, that conform to the filters - node: { - NOT: { name_EQ: "Tom Hardy" } - } - }, - Person: {} # include all relationships of type `DIRECTED` to a `Person` type - } - } - } -} ----- - - -==== Interface type - -The following example illustrates how to filter the node at the other end of the relationship when it is of an interface type: - -[source, graphql, indent=0] ----- -{ - where: { - deletedRelationship: { - reviewers: { # relationship to an interface type - edge: { - # relationship properties of a relationship of type `REVIEWED` - score_GT: 7 - }, - node: { - # common fields declared by the interface - reputation_GTE: 8 - } - }, - } - } -} ----- - -This example returns events for relationships between the type `Movie` and `Reviewer`, where the score is higher than 7, and the `Reviewer` has a reputation greater or equal to 7. \ No newline at end of file diff --git a/modules/ROOT/pages/subscriptions/getting-started.adoc b/modules/ROOT/pages/subscriptions/getting-started.adoc index 50e20e54..384565dc 100644 --- a/modules/ROOT/pages/subscriptions/getting-started.adoc +++ b/modules/ROOT/pages/subscriptions/getting-started.adoc @@ -9,9 +9,13 @@ This guide shows how to start using subscription capabilities on a GraphQL serve If you use link:https://studio.apollographql.com/[Apollo Studio], make sure to select the link:https://www.npmjs.com/package/graphql-ws[graphql-ws] implementation in the connection settings. ==== +Subscriptions can listen to changes to the database, including changes unrelated to the GraphQL library. == Enable subscription capabilities +Neo4j GraphQL subscriptions rely on Neo4j Change Data Capture. +Make sure to follow the steps described in the link:https://neo4j.com/docs/cdc/current/[CDC Documentation] to enable it in `FULL` mode for your Neo4j instance. + Before using subscriptions on a GraphQL server, you must enable them by passing the `subscriptions` feature to `Neo4jGraphQL`: [source, javascript] @@ -26,7 +30,7 @@ new Neo4jGraphQL({ ---- -== Install dependencies +== Setting up an `@apollo/server` server Then, the next step is to install the following dependencies: @@ -36,8 +40,6 @@ npm i --save ws graphql-ws neo4j-driver @neo4j/graphql express @apollo/server bo ---- -== Setting up an `@apollo/server` server - Add the following code to your `index.js` file to implement a simple `@apollo/server` server with subscriptions (for more options, see link:https://www.apollographql.com/docs/apollo-server/data/subscriptions/[Apollo's documentation]): [source, javascript, indent=no] @@ -133,11 +135,6 @@ async function main() { main(); ---- -[NOTE] -==== -This setup uses the default subscriptions mechanism suitable only for development, testing, and single instance servers. -If you need to scale horizontally for a production-ready application, see xref::subscriptions/scaling.adoc[Horizontal scaling]. -==== == GraphQL subscriptions @@ -168,7 +165,3 @@ mutation { } } ---- - -== Further reading - -Keep reading this section on xref:subscriptions/index.adoc[Subscriptions] for more information and advanced examples. diff --git a/modules/ROOT/pages/subscriptions/index.adoc b/modules/ROOT/pages/subscriptions/index.adoc index c37d2318..59860cb3 100644 --- a/modules/ROOT/pages/subscriptions/index.adoc +++ b/modules/ROOT/pages/subscriptions/index.adoc @@ -9,5 +9,3 @@ This section covers: * xref:subscriptions/getting-started.adoc[Getting started] - How to get started with subscriptions by setting up a basic example. * xref:subscriptions/events.adoc[Events] - How to trigger events when using `@neo4j/graphql`. * xref:subscriptions/filtering.adoc[Filtering] - How to apply filters to subscriptions. -* xref:subscriptions/scaling.adoc[Horizontal scaling] - How to perform horizontal scaling. -* xref:subscriptions/engines.adoc[Subscriptions engines] - How to set up a GraphQL subscription along with a `@neo4j/graphql` server. diff --git a/modules/ROOT/pages/subscriptions/scaling.adoc b/modules/ROOT/pages/subscriptions/scaling.adoc deleted file mode 100644 index 5b8f640b..00000000 --- a/modules/ROOT/pages/subscriptions/scaling.adoc +++ /dev/null @@ -1,48 +0,0 @@ -[[horizontal-scaling]] -:description: This page describes horizontal scaling in Neo4j GraphQL. -= Horizontal scaling - -Horizontally scaling any real time system can be complex, especially when dealing with long lived connections such as WebSockets. -Consider the following example, in which Client A is subscribed to a certain event that is triggered by Client B: - -image::subscriptions/diagram1.svg[title="Basic subscriptions setup example"] - -The server running the GraphQL service does the following: - -. Receives the mutation by Client B. -. Runs the Cypher query on Neo4j. -. Triggers the subscription event to Client A. - -This setup works for a single instance of a `@neo4j/graphql` server. -However, when trying to scale horizontally by adding more GraphQL servers, you may encounter the following situation: - -image::subscriptions/diagram2.svg[title="Subscriptions with 2 servers"] - -In this case, Client A is subscribed to one server. -However, when Client B triggers the mutation, it may query a different server. - -The change happens successfully in the database, and any client connected to the same server receives the subscription event. -However, Client A does **not** receive any update, as the server it's connected to does not get notified of any mutation. - -This is the default behavior of the subscription engine provided by the library, making it unsuitable for use in a horizontally scaled environment. - -== Using PubSub - -One solution to this problem (as well as how `@neo4j/graphql` is intended to work) is to use a PubSub pattern with an external broker to broadcast events through multiple instances. -This can be achieved through different xref::subscriptions/engines.adoc[Subscription engines]. - -Following the previous example, using an intermediate broker to broadcast the events across all instances, the infrastructure would look like this: - -image::subscriptions/diagram3.svg[title="Subscriptions with 2 servers and a message broker"] - -The events are as follow: - -. Client B queries the first server. -. The server performs the mutation in the database. -. The same server sends an event to the broker. -. The broker then notifies every server (broadcast), including the server that originally triggered the event. -. Both servers receive the notification and trigger any event to their subscribed clients. - -== Further reading - -You can find more examples of this type of pattern with `@neo4j/graphql` on xref::subscriptions/engines.adoc[Subscription engines].