Delete Relationship Subscriptions

Subscriptions to DELETE_RELATIONSHIP events will listen for relationships to a node of the specified type that have been deleted.

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.

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.

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.

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.

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.

DELETE_RELATIONSHIP event

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 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.

  • <typename>: 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.

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 Non-reciprocal Relationships section below for more details.

For example, considering the following type definitions:

type Movie {
    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 {
    name: String
}

interface ActedIn @relationshipProperties {
    screenTime: Int!
}

type Reviewer {
    name: String
    reputation: Int
}

interface Reviewed @relationshipProperties {
    score: Int!
}

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:

{
    # 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 occured
                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
            }
        }
    }
}

Examples

Delete Relationships with Standard Types

For example, considering the following type definitions:

type Movie {
    title: String
    genre: String
    actors: [Actor] @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

type Actor {
    name: String
}

interface ActedIn @relationshipProperties {
    screenTime: Int!
}

A subscription to any Movie deleted relationships would look like:

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:

type Movie {
    title: String
    genre: String
    directors: [Director!]! @relationship(type: "DIRECTED", properties: "Directed", direction: IN)
}

union Director = Person | Actor

type Actor {
    name: String
}

type Person {
    name: String
    reputation: Int
}

interface Directed @relationshipProperties {
    year: Int!
}

A subscription to Movie deleted relationships would look like:

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:

type Movie {
    title: String
    genre: String
    reviewers: [Reviewer!]! @relationship(type: "REVIEWED", properties: "Review", direction: IN)
}

interface Reviewer {
    reputation: Int!
}

type Magazine implements Reviewer {
    title: String
    reputation: Int!
}

type Influencer implements Reviewer {
    name: String
    reputation: Int!
}

interface Review {
    score: Int!
}

A subscription to Movie deleted relationships would look like:

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
                    }
                }
            }
        }
    }
}

Non-reciprocal relationships

Considering the following type definitions:

type Movie {
    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 {
    name: String
    movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT)
}

type Person {
    name: String
    reputation: Int
}

union Director = Person | Actor

interface ActedIn @relationshipProperties {
    screenTime: Int!
}

interface 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

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

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:

mutation {
    createMovies(
        input: [
            {
                actors: {
                    create: [
                        {
                            node: {
                                name: "Keanu Reeves"
                            },
                            edge: {
                                screenTime: 420
                            }
                        }
                    ]
                },
                title: "John Wick",
                genre: "Action"
            }
        ]
    ) {
        movies {
            title
            genre
        }
    }
}

mutation {
    deleteMovies(
        where: {
            title: "John Wick"
        }
    ) {
        nodesDeleted
    }
}

Two events will be published (given that we subscribed to DELETE_RELATIONSHIP events on both types):

{
    # 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:

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: "New York Stories"
        }
    ) {
        nodesDeleted
    }
}

Two events will be published (given that we subscribed to DELETE_RELATIONSHIP events on the Movie type):

{
    # 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.

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:

type Actor @node(label: "Person") {
    name: String
    movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT)
}

typePerson {
    name: String
    movies: [Movie!]! @relationship(type: "PART_OF", direction: OUT)
}

type Movie {
    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:

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:

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: "Sweeney Todd"
        }
    ) {
        nodesDeleted
    }
}

Result in the following events:

{
    # 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:

{
    # 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"
            }
        }
    }
},