diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 1dde40c0..83dbafa8 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -40,11 +40,11 @@ *** xref:mutations/delete.adoc[] ** xref:subscriptions/index.adoc[] -*** xref:subscriptions/getting-started.adoc[Getting Started] +*** xref:subscriptions/getting-started.adoc[Getting started] *** xref:subscriptions/events.adoc[Events] *** xref:subscriptions/filtering.adoc[] *** xref:subscriptions/scaling.adoc[] -*** xref:subscriptions/engines.adoc[Subscriptions Engines] +*** xref:subscriptions/engines.adoc[Engines] ** xref:custom-resolvers.adoc[] diff --git a/modules/ROOT/images/subscriptions/diagram1.png b/modules/ROOT/images/subscriptions/diagram1.png deleted file mode 100644 index 634c3950..00000000 Binary files a/modules/ROOT/images/subscriptions/diagram1.png and /dev/null differ diff --git a/modules/ROOT/images/subscriptions/diagram1.svg b/modules/ROOT/images/subscriptions/diagram1.svg new file mode 100644 index 00000000..9c918712 --- /dev/null +++ b/modules/ROOT/images/subscriptions/diagram1.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ROOT/images/subscriptions/diagram2.png b/modules/ROOT/images/subscriptions/diagram2.png deleted file mode 100644 index 3cb8345e..00000000 Binary files a/modules/ROOT/images/subscriptions/diagram2.png and /dev/null differ diff --git a/modules/ROOT/images/subscriptions/diagram2.svg b/modules/ROOT/images/subscriptions/diagram2.svg new file mode 100644 index 00000000..0ce8a6f5 --- /dev/null +++ b/modules/ROOT/images/subscriptions/diagram2.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ROOT/images/subscriptions/diagram3.png b/modules/ROOT/images/subscriptions/diagram3.png deleted file mode 100644 index c47e73c8..00000000 Binary files a/modules/ROOT/images/subscriptions/diagram3.png and /dev/null differ diff --git a/modules/ROOT/images/subscriptions/diagram3.svg b/modules/ROOT/images/subscriptions/diagram3.svg new file mode 100644 index 00000000..28d35606 --- /dev/null +++ b/modules/ROOT/images/subscriptions/diagram3.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/ROOT/pages/subscriptions/engines.adoc b/modules/ROOT/pages/subscriptions/engines.adoc index 0a422593..b4f77f46 100644 --- a/modules/ROOT/pages/subscriptions/engines.adoc +++ b/modules/ROOT/pages/subscriptions/engines.adoc @@ -1,13 +1,14 @@ [[subscription-engines]] -= Engines - -There are different ways to how a GraphQL subscription may be set along with `@neo4j/graphql` server. +:description: This page describes how a GraphQL subscription may be set along with a @neo4j/graphql server. += Subscription engines +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] +[source, javascript, indent=0] ---- new Neo4jGraphQL({ typeDefs, @@ -18,11 +19,13 @@ new Neo4jGraphQL({ }); ---- -This behavior enables a simple subscription system that will work on a single instance, which is ideal for development, testing, and servers that do not require horizontal scaling. +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. [[amqp]] == AMQP -Using subscriptions on a server with multiple instances can be tricky, as described in xref::subscriptions/scaling.adoc[Horizontal Scaling]. + +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. @@ -36,19 +39,22 @@ Some brokers supporting this protocol are: The plugin can be installed with `npm`: -[source, bash] +[source, sh, indent=0] ---- npm install @neo4j/graphql-amqp-subscriptions-engine ---- -NOTE: AMQP 1.0 is **not** supported by this plugin. +[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 will automatically enable the subscriptions with the AMQP broker as a message queue: +This automatically enables the subscriptions with the AMQP broker as a message queue: -[source, javascript] +[souce, javascript, indent=0] ---- const { Neo4jGraphQLAMQPSubscriptionsEngine } = require("@neo4j/graphql-amqp-subscriptions-engine"); @@ -72,33 +78,37 @@ const neoSchema = new Neo4jGraphQL({ === 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`. +* **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. +** **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`. - -==== Methods +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 +== 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 will publish a new event to the broker. -* Optionally, an `init` method returning a promise that will be called on `getSchema`. +* 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] +[souce, javascript, indent=0] ---- // Note: This is an example of a custom subscription behavior and not a production ready redis implementation. class CustomRedisSubscriptionEngine { @@ -139,9 +149,10 @@ const neoSchema = new Neo4jGraphQL({ ---- Note that extra properties and methods are often needed to handle the connection to the broker. -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 will be properly handled. +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: @@ -152,6 +163,7 @@ class CustomRedisEngine implements Neo4jGraphQLSubscriptionsEngine {} [NOTE] ==== -Events are sent to the class in order, however, order is not guaranteed once these events have been broadcasted through a broker. +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 5d924fac..e1e548bd 100644 --- a/modules/ROOT/pages/subscriptions/events.adoc +++ b/modules/ROOT/pages/subscriptions/events.adoc @@ -1,26 +1,30 @@ [[subscription-events]] -= Subscription Events +:description: This page covers a variety of subscription options offered by the Neo4j GraphQL Library. += Subscription events -[[create]] -== Create Subscriptions +This page covers a variety of subscription options offered by the Neo4j GraphQL Library. -Subscriptions to `CREATE` events will listen to newly created nodes. A new event will be triggered for each new node. -The event will contain the node's properties. +[NOTE] +=== +Only changes made through `@neo4j/graphql` should trigger the events here described. +Changes made directly to the database or using the xref::reference/directives/cypher.adoc[`@cypher` directive] will **not** trigger any event. +=== -NOTE: Only nodes created will trigger this event, new relationships will **not** trigger the event. +== `CREATE` -=== `CREATE` event +Subscriptions to `CREATE` events listen *only* to newly created nodes, not new relationships. +In this occasion, a new event is triggered for each new node, containing its properties. -A subscription to a type can be made with the top-level subscription `[type]Created`. The subscription will contain the following fields: +This action is performed with the top-level subscription `[type]Created`, which contains the following fields: -* `event`: The event triggering this subscription, in this case it will always be `"CREATE"`. -* `created`: The properties of the newly created node, only top-level properties, without relationships, are available. -* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. +* `event`: the event triggering this subscription (in this case, `CREATE`). +* `created`: top-level properties of the newly created node, without relationships. +* `timestamp`: the timestamp in which the mutation was made. +If a same query triggers multiple events, they should have the same timestamp. -==== Example -Considering the following type definitions: +As an example, consider the following type definitions: -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -28,9 +32,34 @@ type Movie { } ---- -A subscription to any `Movie` created would look like: +Note, however, that only changes made through `@neo4j/graphql` should trigger events. +Changes made directly to the database or using the xref::reference/directives/cypher.adoc[`@cypher` directive] will **not** trigger any event. -[source, graphql] +== `CREATE` + +Subscriptions to `CREATE` events listen *only* to newly created nodes, not new relationships. +In this occasion, a new event is triggered for each new node, containing its properties. + +This action is performed with the top-level subscription `[type]Created`, which contains the following fields: + +* `event`: the event triggering this subscription (in this case, `CREATE`). +* `created`: top-level properties of the newly created node, without relationships. +* `timestamp`: the timestamp in which the mutation was made. +If a same query triggers multiple events, they should have the same timestamp. + +As an example, consider the following type definitions: + +[source,graphql,indent=0] +---- +type Movie { + title: String + genre: String +} +---- + +A subscription to any newly created node of the `Movie` type should look like this: + +[source,graphql,indent=0] ---- subscription { movieCreated { @@ -45,26 +74,22 @@ subscription { ---- [[update]] -== Update Subscriptions - -Subscription to `UPDATE` events will listen to node properties changes. A new event will be triggered for each mutation that modifies the node top-level properties. - -NOTE: Update events will only be triggered if any of the properties have changed. An update that doesn't modify the properties will be ignored. +== `UPDATE` -=== `UPDATE` event +Subscriptions to `UPDATE` events listen *only* to node properties changes, not updates to other fields. +In this occasion, a new event is triggered for each mutation that modifies the node top-level properties. -A subscription to a type can be made with the top-level subscription `[type]Updated`. The subscription will contain the following fields: +This action is performed with the top-level subscription `[type]Updated`, which contains the following fields: -* `event`: The event triggering this subscription, in this case it will always be `"UPDATE"`. -* `updated`: The properties of the updated node, after modification. Only top-level properties, without relationships, are available. -* `previousState`: The old properties of the node, right before the update event. Only top-level properties are available. -* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. +* `event`: the event triggering this subscription (in this case, `UPDATE`). +* `updated`: top-level properties of the updated node, without relationships. +* `previousState`: the previous top-level properties of the node, before the `UPDATE` event. +* `timestamp`: the timestamp in which the mutation was made. +If a same query triggers multiple events, they should have the same timestamp. +As an example, consider the following type definitions: -==== Example -Considering the following type definitions: - -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -72,9 +97,9 @@ type Movie { } ---- -A subscription to any `Movie` updated could look like: +A subscription to any node of the `Movie` type with its properties recently updated should look like this: -[source, graphql] +[source,graphql,indent=0] ---- subscription MovieUpdated { movieUpdated { @@ -91,25 +116,19 @@ subscription MovieUpdated { } ---- -[[delete]] -== Delete Subscriptions - -Subscriptions to `DELETE` events will trigger on deleted nodes. +== `DELETE` -NOTE: Only deleted nodes will trigger this event, deleted relationships will **not** trigger any event. +Subscriptions to `DELETE` events listen *only* to nodes being deleted, not deleted relationships. +This action is performed with the top-level subscription `[type]Deleted`, which contains the following fields: -=== `DELETE` event +* `event`: the event triggering this subscription (in this case, `DELETE`). +* `deleted`: top-level properties of the deleted node, without relationships. +* `timestamp`: the timestamp in which the mutation was made. +If a same query triggers multiple events, they should have the same timestamp. -A subscription to a type can be made with the top-level subscription `[type]Deleted`. The subscription will contain the following fields: +As an example, consider the following type definitions: -* `event`: The event triggering this subscription, in this case it will always be `"DELETE"`. -* `deleted`: The top-level properties of the deleted node, these will be the properties right before the node was deleted. Relationships are not available. -* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. - -==== Example -Considering the following type definitions: - -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -117,9 +136,9 @@ type Movie { } ---- -A subscription to any deleted `Movie` would look like: +A subscription to any deleted nodes of the `Movie` type should look like this: -[source, graphql] +[source,graphql,indent=0] ---- subscription { movieDeleted { @@ -133,39 +152,51 @@ subscription { ---- [[create_relationship]] -== Create Relationship Subscriptions - -Subscriptions to `CREATE_RELATIONSHIP` events will listen for newly created relationships to a node of the specified type. +== `CREATE_RELATIONSHIP` -NOTE: This subscription operation is **only** available for types that define relationship fields. +Subscriptions to `CREATE_RELATIONSHIP` events listen for newly created relationships to a node of the specified type. -As relationship-specific information, the event will contain the relationship field name, as well as an object containing all relationship field names of the specified type. This object will be populated with properties according to the newly created relationship. +[NOTE] +=== +This subscription operation is **only** available for types that define relationship fields. +=== -NOTE: A new event will be triggered for each new relationship. +== `CREATE_RELATIONSHIP` -This means that, if the type targeted for the subscriptions defines two or more relationships in the schema and one of each relationships are created following a mutation operation, the number of events triggered will be equivalent to the number of relationships created. +Subscriptions to `CREATE_RELATIONSHIP` events listen to new relationships being created and contain information about the connected nodes. +These events: -Each event will have the relationships object populated with the created relationship's properties for one single relationship name only - all other relationship names will have a null value. +* 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. -The event will also contain the properties of the nodes at both ends of 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. +=== -NOTE: The `CREATE_RELATIONSHIP` events represent new relationships being created and contain information about the nodes at each end of the new relationship. However, the connected nodes may or may not have previously existed. To subscribe to the node's updates, you need to use one of the `CREATE` or `UPDATE` subscriptions. +Subscriptions to `CREATE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipCreated`, which contains the following fields: -=== `CREATE_RELATIONSHIP` event +* `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. -A subscription to a type can be made with the top-level subscription `[type]RelationshipCreated`. The subscription will contain the following fields: +[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#create-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. +=== -* `event`: The event triggering this subscription, in this case it will always be `"CREATE_RELATIONSHIP"`. -* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. -* ``: The properties of the node target to the subscription. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `CREATE_RELATIONSHIP` took place. -* `relationshipFieldName`: The field name of the newly created relationship, as part of the node target to the subscription. -* `createdRelationship`: An object having as keys all field names of the node target to the subscription which represent its relationships. For any given event, all field names except the one corresponding to `relationshipFieldName` will be null. The field name equal to `relationshipFieldName` will 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. Note these are the properties before the operation that triggered the `CREATE_RELATIONSHIP` took place. +As an example, consider the following type definitions: -NOTE: Irrespective of the relationship direction in the database, the `CREATE_RELATIONSHIP` event is bound to the type targeted for the subscription. The consequence is that - given a relationship between types A and B that is not reciprocal (that is, in the GraphQL schema type A defines a relationship to B but B does **not** define a relationship to A) and a GraphQL operation that creates the relationship between them - even though the two nodes will be connected in the database, the `CREATE_RELATIONSHIP` event will only be returned to the subscription to type A. Check out the xref:subscriptions/events.adoc#create-non-reciprocal-relationships[Non-reciprocal Relationships] section below for more details. - -For example, considering the following type definitions: - -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -192,9 +223,10 @@ interface Reviewed @relationshipProperties { } ---- -An ongoing subscription to created relationships on the `Movie` type, upon a mutation creating an `Actor` named `Tom Hardy` and a `Reviewer` named `Jane` to a `Movie` titled `Inception` would receive the following events: +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] +[source,graphql,indent=0] ---- { # 1 - relationship type `ACTED_IN` @@ -237,13 +269,11 @@ An ongoing subscription to created relationships on the `Movie` type, upon a mut } ---- -=== Examples - -==== Create Relationship with Standard Types +=== Standard types -For example, considering the following type definitions: +For another example, this time creating a relationship with standard types, consider the following type definitions: -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -260,9 +290,9 @@ interface ActedIn @relationshipProperties { } ---- -A subscription to any `Movie` created relationships would look like: +A subscription to any `Movie` with newly created relationships should look like this: -[source, graphql] +[source,graphql,indent=0] ---- subscription { movieRelationshipCreated { @@ -285,17 +315,15 @@ subscription { } ---- -==== Create 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. +=== Abstract types -These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a **Concrete Type**. +When using abstract types with relationships, you need to specify one or more of the corresponding concrete types when performing the subscription operation. -===== Union Example +These types are generated by the library and conform to the format `[type]EventPayload`, where `[type]` is a concrete type. -Considering the following type definitions: +As an example, consider the following type definitions: -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -319,9 +347,9 @@ interface Directed @relationshipProperties { } ---- -A subscription to any `Movie` created relationships would look like: +A subscription to any `Movie` newly created relationships should look like this: -[source, graphql] +[source,graphql,indent=0] ---- subscription { movieRelationshipCreated { @@ -350,10 +378,11 @@ subscription { } ---- -===== Interface Example -Considering the following type definitions: +=== Interface -[source, graphql] +For an example in which a relationship is created with an interface, consider the following type definitions: + +[source,graphql,indent=0] ---- type Movie { title: String @@ -380,9 +409,9 @@ interface Review @relationshipProperties { } ---- -A subscription to any `Movie` created relationships would look like: +A subscription to any `Movie` newly created relationships should look like this: -[source, graphql] +[source,graphql,indent=0] ---- subscription { movieRelationshipCreated { @@ -413,12 +442,13 @@ subscription { } ---- -[[create-non-reciprocal-relationships]] -==== Non-reciprocal relationships +=== Non-reciprocal relationships -Considering the following type definitions: +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. -[source, graphql] +To illustrate that, consider the following type definitions: + +[source,graphql,indent=0] ---- type Movie { title: String @@ -448,17 +478,17 @@ interface Directed @relationshipProperties { } ---- -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. +Note that the type definitions contain two relationships: -`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 created relationships for the 3 types defined above: +* `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. -===== Movie +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: -[source, graphql] +.`Movie` type +[source,graphql,indent=0] ---- subscription { movieRelationshipCreated { @@ -493,13 +523,8 @@ subscription { } ---- -==== Person - -As the `Person` type does not define any relationships, it is **not** possible to subscribe to `CREATE_RELATIONSHIP` events for this type. - -===== Actor - -[source, graphql] +.`Actor` type +[source,graphql,indent=0] ---- subscription { actorRelationshipCreated { @@ -523,11 +548,11 @@ subscription { } ---- -The presence of the `movie` field inside of `createdRelationship` for the `actorRelationshipCreated` subscription reflects the fact that the `ACTED_IN` typed relationship is reciprocal. +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 made, such as by running a mutation as follows: +Therefore, when a new relationship of this type is created, such as by running this mutation: -[source, graphql] +[source,graphql,indent=0] ---- mutation { createMovies( @@ -558,9 +583,9 @@ mutation { } ---- -Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` events on both types): +Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on both types: -[source, graphql] +[source,graphql,indent=0] ---- { # from `movieRelationshipCreated` @@ -601,9 +626,9 @@ Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` } ---- -Since the `DIRECTED` relationship between types `Movie` and `Director` is **not** reciprocal, executing a mutation as follows: +Now, since the `DIRECTED` relationship between types `Movie` and `Director` is *not* reciprocal, executing this mutation: -[source, graphql] +[source,graphql,indent=0] ---- mutation { createMovies( @@ -649,9 +674,9 @@ mutation { } ---- -Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` events on the `Movie` type): +Should prompt two events, in case you have subscribed to `CREATE_RELATIONSHIP` events on the `Movie` type: -[source, graphql] +[source,graphql,indent=0] ---- { # relationship 1 - from `movieRelationshipCreated` @@ -694,19 +719,15 @@ Two events will be published (given that we subscribed to `CREATE_RELATIONSHIP` } ---- -=== Special Considerations - -[[connect-same-label]] -==== Types using the same Neo4j label +=== Types using the same Neo4j label -One case that deserves special consideration is overriding the label in Neo4j for a specific GraphQL type. +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. -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: +As an example, consider these type definitions: -[source, graphql] +[source,graphql,indent=0] ---- type Actor @node(label: "Person") { name: String @@ -726,13 +747,14 @@ type Movie { } ---- -Although we have 3 GraphQL types, in Neo4j there will only ever be 2 types of nodes: labeled `Movie` or labeled `Person`. +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 will be one event for each of the 2 types. +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] +[source,graphql,indent=0] ---- subscription { movieRelationshipCreated { @@ -778,9 +800,9 @@ subscription { } ---- -Running a mutation as follows: +Running a mutation such as: -[source, graphql] +[source,graphql,indent=0] ---- mutation { createMovies( @@ -817,9 +839,9 @@ mutation { } ---- -Results in the following events: +Should result in this: -[source, graphql] +[source,graphql,indent=0] ---- { # relationship 1 `people` - for GraphQL types `Movie`, `Person` @@ -929,9 +951,9 @@ Results in the following events: }, ---- -Had we subscribed to `Person` as well, we would have received two more events: +In case you have subscribed to `Person` as well, you should receive two more events: -[source, graphql] +[source,graphql,indent=0] ---- { # relationship 1 `movies` - for GraphQL types `Person`, `Movie` @@ -969,40 +991,43 @@ Had we subscribed to `Person` as well, we would have received two more events: }, ---- -[[delete_relationship]] -== Delete Relationship Subscriptions - -Subscriptions to `DELETE_RELATIONSHIP` events will listen for relationships to a node of the specified type that have been deleted. - -NOTE: This subscription operation is **only** available for types that define relationship fields. - -As relationship-specific information, the event will contain the relationship field name, as well as an object containing all relationship field names of the specified type. This object will be populated with properties according to the deleted relationship. - -NOTE: A new event will be triggered for each deleted relationship. - -This means that, if the type targeted for the subscriptions defines two or more relationships in the schema and one of each relationships are deleted following a mutation operation, the number of events triggered will be equivalent to the number of relationships deleted. +== `DELETE_RELATIONSHIP` -Each event will have the relationships object populated with the deleted relationship's properties for one single relationship name only - all other relationship names will have a null value. +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: -The event will also contain the properties of the nodes at both ends of the relationship, as well as the properties of the new relationship, if any. +* 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: The `DELETE_RELATIONSHIP` events represent relationships being deleted and contain information about the nodes at each end of the new relationship. However, the disconnected nodes may or may not have been deleted in the process. To subscribe to the node's updates, you need to use the `DELETE` subscriptions. +[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. +=== -=== `DELETE_RELATIONSHIP` event +Subscriptions to `DELETE_RELATIONSHIP` events can be made with the top-level subscription `[type]RelationshipDeleted`, which contains the following fields: -A subscription to a type can be made with the top-level subscription `[type]RelationshipDeleted`. The subscription will contain 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. -* `event`: The event triggering this subscription, in this case it will always be `"DELETE_RELATIONSHIP"`. -* `timestamp`: The timestamp in which the mutation was made. Note that multiple events may come with the same timestamp if triggered by the same query. -* ``: The properties of the node target to the subscription. Only top-level properties, without relationships, are available. Note these are the properties before the operation that triggered the `DELETE_RELATIONSHIP` took place. -* `relationshipFieldName`: The field name of the deleted relationship, as part of the node target to the subscription. -* `deletedRelationship`: An object having as keys all field names of the node target to the subscription which represent its relationships. For any given event, all field names except the one corresponding to `relationshipFieldName` will be null. The field name equal to `relationshipFieldName` will 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. Note these are the properties before the operation that triggered the `DELETE_RELATIONSHIP` 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 diconnected in the database), the `DELETE_RELATIONSHIP` event should only return the subscription to the type A. +=== -NOTE: Irrespective of the relationship direction in the database, the `DELETE_RELATIONSHIP` event is bound to the type targeted for the subscription. The consequence is that - given a relationship between types A and B that is not reciprocal (that is, in the GraphQL schema type A defines a relationship to B but B does **not** define a relationship to A) and a GraphQL operation that deletes the relationship between them - even though the two nodes will be disconnected in the database, the `DELETE_RELATIONSHIP` event will only be returned to the subscription to type A. Check out the xref:subscriptions/events.adoc#delete-non-reciprocal-relationships[Non-reciprocal Relationships] section below for more details. +As an example, consider these type definitions: -For example, considering the following type definitions: - -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String @@ -1029,9 +1054,10 @@ interface Reviewed @relationshipProperties { } ---- -An ongoing subscription to deleted relationships from the `Movie` type, upon a mutation deleting then `Actor` named `Tom Hardy` and the `Reviewer` named `Jane` from a `Movie` titled `Inception` would receive the following events: +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] +[source,graphql,indent=0] ---- { # 1 - relationship type `ACTED_IN` @@ -1074,13 +1100,11 @@ An ongoing subscription to deleted relationships from the `Movie` type, upon a m } ---- -=== Examples +=== Standard types -==== Delete Relationships with Standard Types +As an example, consider these type definitions: -For example, considering the following type definitions: - -[source, graphql] +[source,graphql,indent=0] ---- type Movie { title: String diff --git a/modules/ROOT/pages/subscriptions/filtering.adoc b/modules/ROOT/pages/subscriptions/filtering.adoc index 9fcdbb3b..9c311ec2 100644 --- a/modules/ROOT/pages/subscriptions/filtering.adoc +++ b/modules/ROOT/pages/subscriptions/filtering.adoc @@ -1,11 +1,14 @@ -[[create]] +[[filtering]] +:description: This page covers how to apply filters to subscriptions in the Neo4j GraphQL Library. = Filtering -Filtering can only be applied at the root of the Subscription operation. +This page covers how to apply filters to subscriptions in the Neo4j GraphQL Library. +Note, however, that: -NOTE: Aggregations are not supported on subscription types, so there is currently no way to apply filter on these fields. +* Filtering can only be applied at the root of the subscription operation. +* Aggregations are not supported on subscription types and there is currently no available method. -A subscription can be created to target the changes to a node (Create/Update/Delete) or to a relationship (Create/Delete). +A subscription can be created to target the changes to a node (`create`/`update`/`delete``) or to a relationship (`create`/`delete``). While the format slightly differs depending on whether the subscription targets a node or a relationship, providing a `where` argument allows for filtering on the events that are returned to the subscription. @@ -15,19 +18,23 @@ When creating a subscription, a number of operators are available for different === Equality operators -All types can be tested for either equality or non-equality. For the `Boolean` type, these are the only available comparison operators. +All types can be tested for either equality or non-equality. +For the `Boolean` type, these are the only available comparison operators. [[filtering-numerical-operators]] === Numerical operators -The following comparison operators are available for numeric types (`Int`, `Float`, xref::type-definitions/types/scalar.adoc[`BigInt`]) +The following comparison operators are available for numeric types (`Int`, `Float`, xref::type-definitions/types/index.adoc[`BigInt`]): * `_LT` * `_LTE` * `_GTE` * `_GT` -NOTE: Filtering on xref::/type-definitions/types/temporal.adoc[Temporal Types] and xref::/type-definitions/types/spatial.adoc[Spatial Types]: is not yet supported. +[NOTE] +==== +Filtering on xref::type-definitions/types/temporal.adoc[Temporal types] and xref::type-definitions/types/spatial.adoc[Spatial types] is not currently supported. +==== === String comparison @@ -39,31 +46,31 @@ The following case-sensitive comparison operators are only available for use on === Array comparison -The following two comparison operators are available on non-array fields, and accept an array argument: +The following operator is available on non-array fields, and accepts an array argument: * `_IN` -Conversely, the following operators are available on array fields, and accept a single argument: +Conversely, the following operator is available on array fields, and accepts a single argument: * `_INCLUDES` -These four operators are available for all types apart from `Boolean`. +These operators are available for all types apart from `Boolean`. -=== AND, OR operators +=== `AND`/`OR` operators Complex combinations of operators are possible using the `AND`/ `OR` operators. +They accept as argument an array of items of the same format as the `where` argument. -`AND`/`OR` operators accept as argument an array of items of the same format as the `where` argument. - -Check out a usage example in the xref:subscriptions/filtering.adoc#combining-operators[Combining operators] section below. +Check xref:subscriptions/filtering.adoc#combining-operators[Combining operators] for an example. [[node-events-usage]] == Subscribing to node events -The `where` argument allows for specifying filters on top-level properties of the targeted nodes. -Only events matching these properties and type will be returned to the subscription. +The `where` argument allows specifying filters on top-level properties of the targeted nodes. +Only events matching these properties and type are returned to the subscription. + +As an example, consider the following type definitions: -Considering the following type definitions: [source, graphql, indent=0] ---- type Movie { @@ -73,10 +80,12 @@ type Movie { releasedIn: Int } ---- -Below are some example of how filtering can be applied when creating a subscription. -=== Create -We can filter our movies by their genre: +Now, here is how filtering can be applied when creating a subscription: + +=== `CREATE` + +To filter subscriptions to `create` operations for movies by their genre, here is how to do it: [source, graphql, indent=0] ---- @@ -89,12 +98,16 @@ subscription { } ---- -This way, only newly created movies with the genre `"Drama"` will trigger events to this subscription. +This way, only newly created movies with the genre `"Drama"` trigger events to this subscription. + +[NOTE] +==== +The `where` argument only filters by properties set at the moment of creation. +==== -NOTE: `where` will only filter by properties set at the moment of creation. +=== `UPDATE` -=== Update -We can filter our movies with the average rating bigger than 8: +Here is how to filter subscription to `update` operations in movies with average rating bigger than 8: [source, graphql, indent=0] ---- @@ -107,9 +120,14 @@ subscription { } ---- -This way, we will only receive events triggered by movies with the average rating bigger than 8 being modified. +This way, you should only receive events triggered by movies with the average rating bigger than 8 when they are modified. -NOTE: `Where` will only filter by existing properties before the update. +[NOTE] +==== +The `where` argument only filters properties that already existed *before* the update. +==== + +This is how these events may look like: [source, graphql, indent=0] ---- @@ -140,10 +158,11 @@ mutation { } ---- -Therefore, given the above subscription, these GraphQL operations will only be triggered for `"The Matrix"` movie. +Therefore, given the previously described subscription, these GraphQL operations should only triggered for `"The Matrix"` movie. + +=== `DELETE` -=== Delete -we can filter our movies by their genre with the `NOT` filter: +Here is how to filter subscription to `delete` operations in movies by their genre, using the `NOT` filter: [source, graphql, indent=0] ---- @@ -156,18 +175,21 @@ subscription { } ---- -This way, only deleted movies of all genres except for `"Comedy"` will trigger events to this subscription. +This way, only deleted movies of all genres except for `"Comedy"` should trigger events to this subscription. -NOTE: `Where` will only filter by existing properties right before deletion. +[NOTE] +==== +The `where` argument only filters properties that already existed before the deletion process. +==== [[combining-operators]] === Combining operators -All above-mentioned operators can be combined using the `AND`/`OR`/`NOT` operators. +All previously mentioned operators can be combined using the `AND`, `OR`, and `NOT` operators. They accept an array argument with items of the same format as the `where` argument, which means they can also be nested to form complex combinations. -Say we like comedy movies except for romantic comedies from early 2000, although our favorite movies are ones from the Matrix Trilogy. -We could subscribe to any updates that we are interested in as follows: +As an exampke, consider a user who likes comedy movies, but not romantic comedies from early 2000, and who has the Matrix Trilogy as their favorite titles. +They could subscribe to any updates that cover this particular set of interests as follows: [source, graphql, indent=0] ---- @@ -193,13 +215,21 @@ subscription { == Subscribing to relationship events -When subscribing to relationship events, the `where` argument still allows for specifying filters on the top-level properties of the targeted nodes, and 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 described above, and the usage is very similar to the one in xref:subscriptions/filtering.adoc#node-events-usage[Subscribing to node 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]. -The relationship-related filtering logic is even more powerful, as filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship when having relationships 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 will be returned in the subscription. +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. -You can further filter each relationship field by node and relationship properties. As per usual, these fields are combined in the resulting filter with a xref:subscriptions/filtering.adoc#filter-logical-and[logical `AND`]. +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: -Considering the following type definitions: [source, graphql, indent=0] ---- type Movie { @@ -218,6 +248,7 @@ type Actor { ---- The format of the `where` argument is: + [source, graphql, indent=0] ---- { @@ -240,10 +271,11 @@ The format of the `where` argument is: } ---- -Below are some example of how filtering can be applied when creating a subscription to relationship events. +The following sections feature examples of how filtering can be applied when creating a subscription to relationship events. + +=== Newly created relationship -=== Create Relationship -The following example filters the subscriptions to newly created relationships that are connecting a `Movie` from genres other than "Drama", to an `Actor` with a screen time bigger than 10: +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] ---- @@ -264,10 +296,14 @@ subscription { } ---- -NOTE: `where` will only filter by properties set at the moment of creation. +[NOTE] +==== +The `where` argument only filters already existing properties at the moment of the relationship creation. +==== + +=== Newly deleted relationship -=== Delete Relationship -The following example filters the subscriptions to deleted relationships that were connecting a `Movie` of genre Comedy or Adventure to an `Actor` named "Jim Carrey": +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] ---- @@ -288,15 +324,21 @@ subscription { } ---- -NOTE: `Where` will only filter by existing properties right before deletion. - +[NOTE] +==== +The `where` argument only filters properties that already existed before the relationship deletion. +==== === Relationship-related filters -In addition to filtering on node or relationship properties, the relationship-related filtering logic is even more powerful, as filters can also express the expected relationship field, or the expected concrete type at the other end of the relationship when having relationships to Abstract Types. -The following examples are valid for both `CREATE_RELATIONSHIP`/`DELETE_RELATIONSHIP` events. Their purpose is to illustrate the various ways in which a subscription to a relationship event can be filtered in a variety of ways. +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 { @@ -387,15 +429,17 @@ subscription MovieRelationshipDeleted($where: MovieRelationshipDeletedSubscripti } ---- -Given the above subscription, you can use the following where inputs in the GraphQL variable values to get different results. +You can use the following `where` inputs in the GraphQL variable values to get different results: + +==== Filtering via implicit/explicit declaration -=== 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. -Implicit or explicit declaration is used to filter on the 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`. -For example, when subscribing to created or deleted relationships to a `Movie` we might only be interested in the relationship of type `ACTED_IN`, indifferent to the properties of the `Actor` node or of the relationship to it. Note that 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: -By explicitly specifying the `actors` field name, we filter-out events to other relationship properties: [source, graphql, indent=0] ---- { @@ -407,7 +451,8 @@ By explicitly specifying the `actors` field name, we filter-out events to other } ---- -If we were interested in `Actor` nodes conforming to some filters, for example with the name starting with the letter "A", it is no different than when xref:subscriptions/filtering.adoc#node-events-usage[Subscribing to node events]: +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] ---- { @@ -423,7 +468,8 @@ If we were interested in `Actor` nodes conforming to some filters, for example w } ---- -Or we could also be interested in the relationship itself conforming to some filters, like the `Actor` to have spent no more than 40 minutes in the `Movie`: +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] ---- { @@ -442,7 +488,9 @@ Or we could also be interested in the relationship itself conforming to some fil } ---- -Multiple relationship types can be included in the returned subscriptions by explicitly specifying the corresponding field names like so: +Multiple relationship types can also be included in the returned subscriptions by explicitly specifying the corresponding field names. +For instance: + [source, graphql, indent=0] ---- { @@ -456,7 +504,8 @@ Multiple relationship types can be included in the returned subscriptions by exp } ---- -In case we are interested in all relationship types, we can either express this implicitly by not specifying any: +Now, if you are interested in all relationship types, you can either express this implicitly by not specifying any: + [source, graphql, indent=0] ---- { @@ -465,7 +514,9 @@ In case we are interested in all relationship types, we can either express this } } ---- + Or explicitly by specifying the field names of all the relationships connected to the type targeted for the subscription: + [source, graphql, indent=0] ---- { @@ -481,9 +532,10 @@ Or explicitly by specifying the field names of all the relationships connected t } ---- -NOTE: As soon as we want to apply **any** filter to **any** of the relationships, explicitly including those that we are interested in is **mandatory** +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: -For example if all relationships should be returned, but we want to filter-out the `REVIEWED` ones with a score less than 7: [source, graphql, indent=0] ---- { @@ -501,7 +553,9 @@ For example if all relationships should be returned, but we want to filter-out t } ---- -Different filters can be applied to the different relationships without any constraints: +Different filters can also be applied to different relationships without any constraints. +For example: + [source, graphql, indent=0] ---- { @@ -527,15 +581,28 @@ Different filters can be applied to the different relationships without any cons ---- [[filter-logical-or]] -NOTE: Note that in the above, there is an implicit logical `OR` between the `actors`, `directors` and `reviewers`, relationship fields. I.e. a relationship of **either** type `ACTED_IN` **or** of type `DIRECTED` **or** of type `REVIEWED` will trigger the subscription above. + +[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: Note that there is an implicit logical `AND` between the `edge` and `node` fields inside of the `actors` relationship field. I.e. a relationship of type `ACTED_IN` with the property `screenTime` less than 60 **and** a target node with name in ["Tom Hardy", "George Clooney"] will trigger the subscription. +[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 -=== Abstract Types +The following sections describe how to filter subscriptions using abstract types. -==== Union Type +==== Union type + +This example illustrates how to filter the node at the other end of the relationship when it is of a union type: -The following example illustrates how to filter on the node at the other end of the relationship when it is of a Union type: [source, graphql, indent=0] ---- { @@ -565,12 +632,13 @@ The following example illustrates how to filter on the node at the other end of } ---- -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. +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: 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. +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: -What about the example above that did not explicitly specify the Concrete types? [source, graphql, indent=0] ---- { @@ -582,8 +650,11 @@ What about the example above that did not explicitly specify the Concrete types? } ---- -Following the same logic as for the relationship field names, when nothing is explicitly provided then all is accepted. Thus relationships of type `DIRECTED` between a `Movie` and any of the Concrete types that make up the Union type `Director` will be returned to the subscription. -It is therefore equivalent to the following: +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] ---- { @@ -598,7 +669,8 @@ It is therefore equivalent to the following: } ---- -Of course, it follows that explicitly specifying a Concrete type will exclude the other from the returned events: +Note that explicitly specifying a concrete type excludes the others from the returned events: + [source, graphql, indent=0] ---- { @@ -612,9 +684,11 @@ Of course, it follows that explicitly specifying a Concrete type will exclude th } ---- -In this case, only relationships of type `DIRECTED` between a `Movie` and an `Actor` will be returned to the subscription, those between a `Movie` and a `Person` being filtered out. +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] ---- { @@ -632,7 +706,8 @@ One reason why this might be done is to include some filters on the `Actor` type } ---- -To include filters on the `Actor` type but also include `Person` type in the result, we need to make the intent explicit: +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] ---- { @@ -652,9 +727,10 @@ To include filters on the `Actor` type but also include `Person` type in the res ---- -==== Interface 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: -The following example illustrates how to filter on the node at the other end of the relationship when it is of an Interface type: [source, graphql, indent=0] ---- { @@ -685,12 +761,20 @@ The following example illustrates how to filter on the node at the other end of } ---- -The above will return events for relationships between the type `Movie` and `Reviewer`, where the score is greater than 7 and the `Reviewer` is a Person named "Jane Doe" with a reputation greater or equal to 7, or the `Reviewer` is a Magazine with the reputation of 8. +This example returns events for relationships between the type `Movie` and `Reviewer`, where the score is higher than 7, and the `Reviewer` is a `Person` named "Jane Doe", with a reputation greater or equal to 7, or the `Reviewer` is a `Magazine` with the reputation of 8. -NOTE: Notice how the reputation field is part of the Interface type, and can thus be specified in 3 ways: inside the `node` key, inside each Concrete type, or in both places. When specified in both places, the filter is composed with a logical `AND`. Type `Person` overrides the `reputation_GTE` operator so the final filter is `reputation_GTE: 7`, while type `Magazine` composes the original operator so the final filter is the interval `reputation_GTE: 8 && reputation_LT: 9`. +[NOTE] +==== +Note how the reputation field is part of the interface type, and can thus be specified in 3 ways: inside the `node` key, inside each concrete type, or in both places. +When specified in both places, however, the filter is composed with a logical `AND`. +Type `Person` then overrides the `reputation_GTE` operator, so the final filter is `reputation_GTE: 7`. +Likewise, type `Magazine` composes the original operator so the final filter is the interval `reputation_GTE: 8 && reputation_LT: 9`. +==== + + +To get all relationships of type `REVIEWED` with a certain score returned, you can use implicit filtering, such as: -To get all relationships of type `REVIEWED` with a certain score returned, we can make use of the implicit filtering like so: [source, graphql, indent=0] ---- { @@ -706,7 +790,8 @@ To get all relationships of type `REVIEWED` with a certain score returned, we ca } ---- -Even for relationships of type `REVIEWED` to a `Reviewer` of a specific reputation, we can still make use of the implicit filtering: +Implicit filtering can still be used, even for relationships of type `REVIEWED` to a `Reviewer` of a specific reputation: + [source, graphql, indent=0] ---- { @@ -722,7 +807,8 @@ Even for relationships of type `REVIEWED` to a `Reviewer` of a specific reputati } ---- -It is only when a specific Concrete type needs to be filtered that we need to be explicit in the Concrete types that we are interested in: +It is only when a specific concrete type needs to be filtered that you need to be explicit in the concrete types that you are interested in: + [source, graphql, indent=0] ---- { @@ -743,7 +829,9 @@ It is only when a specific Concrete type needs to be filtered that we need to be } ---- -The above will not include relationships of type `REVIEWED` to the `Magazine` type. We can include them by making the intent explicit: +This example does not include relationships of type `REVIEWED` to the `Magazine` type. +You can do that by making them explicit: + [source, graphql, indent=0] ---- { diff --git a/modules/ROOT/pages/subscriptions/getting-started.adoc b/modules/ROOT/pages/subscriptions/getting-started.adoc index ea58063e..d091fdc6 100644 --- a/modules/ROOT/pages/subscriptions/getting-started.adoc +++ b/modules/ROOT/pages/subscriptions/getting-started.adoc @@ -1,8 +1,17 @@ [[getting-started]] -= Getting Started with Subscriptions +:description: This page shows how to start using subscriptions on a GraphQL server. += Getting started with subscriptions -To get started with subscriptions you need a GraphQL server with subscription capabilities. -You can enable them by passing the `subscriptions` feature to `Neo4jGraphQL`: +This guide shows how to start using subscription capabilities on a GraphQL server. + +[NOTE] +==== +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. +==== + +== Enable subscription capabilities + +Before using subscriptions on a GraphQL server, you must enable them by passing the `subscriptions` feature to `Neo4jGraphQL`: [source, javascript] ---- @@ -15,22 +24,20 @@ new Neo4jGraphQL({ }); ---- -== Example using Apollo and WebSockets -For this example, we will use link:https://www.apollographql.com/[Apollo] and link:https://github.com/enisdenjo/graphql-ws[graphql-ws]. +== Install dependencies -=== Setting up the server - -Install the following dependencies: +Then, the next step is to install the following dependencies: [source, bash] ---- npm i --save ws graphql-ws neo4j-driver @neo4j/graphql express @apollo/server body-parser cors ---- -The following code implements a simple `@apollo/server` server with subscriptions. You can find more examples and documentation -on subscriptions in link:https://www.apollographql.com/docs/apollo-server/data/subscriptions/[Apollo's documentation]. +== Set up an `@apollo/server` server -[source, javascript] +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] ---- import { ApolloServer } from "@apollo/server"; import { expressMiddleware } from "@apollo/server/express4"; @@ -125,15 +132,18 @@ main(); [NOTE] ==== -This setup will use the default subscriptions mechanism suitable only for development, testing and single instance servers. +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 -With the previous server running, we have subscriptions available for `Movie` and `Actor`. We can subscribe to new movies created with the following statement: +== GraphQL subscriptions -[source, graphql] +With the previous server running, the available subscriptions are set for `Movie` and `Actor`. +You can subscribe to new movies created with the following statement: + +[source, graphql, indent=0] ---- +graphql subscription { movieCreated(where: { title: "The Matrix" }) { createdMovie { @@ -143,9 +153,10 @@ subscription { } ---- -Any new movie created with the matching title will trigger a subscription. You can try this with the following query: +With that, any new movie created with the matching title will trigger a subscription. +You can try this with the following query: -[source, graphql] +[source, graphql, indent=0] ---- mutation { createMovies(input: [{ title: "The Matrix" }]) { @@ -156,5 +167,6 @@ mutation { } ---- -NOTE: This example uses the link:https://www.npmjs.com/package/graphql-ws[graphql-ws] implementation, if you are using Apollo Studio, make sure -to select "graphql-ws" implementation in connection settings. +== Further reading + +Keep reading this section on xref:subscriptions/index.adoc[Subscriptions] for more information and advanced examples. \ No newline at end of file diff --git a/modules/ROOT/pages/subscriptions/index.adoc b/modules/ROOT/pages/subscriptions/index.adoc index f5f530aa..c37d2318 100644 --- a/modules/ROOT/pages/subscriptions/index.adoc +++ b/modules/ROOT/pages/subscriptions/index.adoc @@ -1,18 +1,13 @@ [[subscriptions]] +:description: This section covers how to use subscriptions with the Neo4j GraphQL Library. = Subscriptions -GraphQL subscriptions add real-time capabilities to your API. -Subscriptions allow to listen for changes on the database and they are available when using `@neo4j/graphql`. +GraphQL subscriptions add real-time capabilities to an API by allowing a server to send data to its clients when a specific event happens. -All xref:subscriptions/events.adoc[events] will be triggered individually, even in events caused by the same Mutation. -Events are fired on the creation or deletion of nodes and relationships, and on the update of nodes. +This section covers: -[NOTE] -==== -Only changes made through `@neo4j/graphql` will trigger events. Changes made directly to the database or using the xref::/type-definitions/directives/cypher.adoc[`@cypher` directive] will **not** trigger any event. -==== - -== Subscriptions with Auth -label:deprecated[] - -Some `auth` clauses can be used with subscriptions. \ No newline at end of file +* 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 index f5eee755..96bbbeaf 100644 --- a/modules/ROOT/pages/subscriptions/scaling.adoc +++ b/modules/ROOT/pages/subscriptions/scaling.adoc @@ -1,49 +1,48 @@ [[horizontal-scaling]] -= Horizontal Scaling +:description: This page describes horizontal scaling in Neo4j GraphQL. += Horizontal scaling -== The Problem -Horizontally scaling any real time system can be tricky, 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_: +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.png[title="Simple subscriptions setup"] +image::subscriptions/diagram1.svg[title="Basic subscriptions setup example"] +The server running the GraphQL service does the following: -The server running the GraphQL service will do the following: +. Receives the mutation by Client B. +. Runs the Cypher query on Neo4j. +. Triggers the subscription event to Client A. -1. Receive the _mutation_ by _Client B_. -2. Run the Cypher Query on Neo4j. -3. Trigger the subscription event to _Client A_. - -This setup works fine for a single instance of a `@neo4j/graphql` server. +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.png[title="Subscriptions with 2 servers"] +image::subscriptions/diagram2.svg[title="Subscriptions with 2 servers"] -In this case, _Client B_ is subscribed to one server. -However, when _Client A_ triggers the mutation, it may query a different server. +In this case, Client B is subscribed to one server. +However, when Client A triggers the mutation, it may query a different server. -The change happens successfully in the database, and any client connected to the same server will receive the subscription event, however, _Client A_ -will **not** receive any update, as the server it's connected to will not get notified of any mutation. +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 an horizontally scaled environment. +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 Mechanisms]. + +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.png[title="Subscriptions with 2 servers and message broker"] +image::subscriptions/diagram3.svg[title="Subscriptions with 2 servers and a message broker"] The events are as follow: -1. _Client B_ will query the first server. -2. The server performs the mutation in the database. -3. The same server sends an event to the broker. -4. The broker will then notify every server (broadcast), including the server that originally triggered the event. -5. Both servers will receive the notification and will trigger any event to their subscribed clients. +. 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 -[NOTE] -==== -You can find examples of this pattern with `@neo4j/graphql` on xref::subscriptions/engines.adoc[Subscription Mechanisms]. -==== +You can find more examples of this type of pattern with `@neo4j/graphql` on xref::subscriptions/engines.adoc[Subscription engines].