Migration to 7.0.0

This is the documentation of the GraphQL Library version 7. For the long-term support (LTS) version 5, refer to GraphQL Library version 5 LTS.

This page lists all breaking changes from the Neo4j GraphQL Library version 6.x to 7.x and how to update it.

How to update

To update your Neo4j GraphQL Library, use npm or the package manager of choice:

npm update @neo4j/graphql

Breaking changes

Here is a list of all the breaking changes from version 6.0.0 to 7.0.0.

Removed the @unique directive

The @unique directive is no longer supported. It cannot always be reliably enforced, potentially leading to data inconsistencies that don’t match the schema.

Removed implicit filtering fields

Implicit equality filtering fields have been removed. Use the dedicated eq field instead:

Before After
{
  movies(where: { title: "The Matrix" }) {
    title
  }
}
{
  movies(where: { title: { eq: "The Matrix" } }) {
    title
  }
}

The implicitEqualFilters option of excludeDeprecatedFields has been removed.

Required @node directive

The @node directive is now required. GraphQL object types without the @node directive are no longer considered Neo4j node representations. Queries and mutations are only generated for types with the @node directive.

Before After
type Movie {
  title: String!
}
type Movie @node {
  title: String!
}

Removed the deprecated options argument

The deprecated options argument has been removed.

Consider the following type definition:

type Movie @node {
  title: String!
}

The following shows the difference for options:

Before After
{
  movies(options: { first: 10, offset: 10, sort: [{ title: ASC }] }) {
    title
  }
}
{
  movies(first: 10, offset: 10, sort: [{ title: ASC }]) {
    title
  }
}

The deprecatedOptionsArgument option of excludeDeprecatedFields has been removed.

Removed the deprecated directed argument

The deprecated directed argument has been removed from queries. This argument previously allowed you to specify whether relationships should be directed or undirected at query time. Use the queryDirection argument of the @relationship directive instead.

The directedArgument option of excludeDeprecatedFields has been removed.

Changed the accepted values of the queryDirection argument of @relationship

Following the removal of the directed argument, the queryDirection argument of the @relationship directive now only accepts two possible values:

  • DIRECTED (default)

  • UNDIRECTED

The following values are no longer supported:

  • DEFAULT_DIRECTED

  • DEFAULT_UNDIRECTED

  • DIRECTED_ONLY

  • UNDIRECTED_ONLY

Removed the deprecated typename_IN filter

The deprecated typename_IN filter has been removed. Use typename instead.

Before After
{
  productions(where: { typename_IN: ["Movie", "Series"] }) {
    title
  }
}
{
  productions(where: { typename: ["Movie", "Series"] }) {
    title
  }
}

The typename_IN option of excludeDeprecatedFields has been removed.

Aggregations are no longer generated for ID fields

ID fields are excluded from aggregation selection sets and aggregation filters.

Removed single element relationships

Single element relationships have been removed in favor of list relationships:

Before After
type Movie @node {
    director: Person @relationship(type: "DIRECTED", direction: "IN")
}
type Movie @node {
    director: [Person!]! @relationship(type: "DIRECTED", direction: "IN")
}

Single element relationships cannot be reliably enforced, leading to data inconsistent with the schema. If the GraphQL model requires one-to-one relationships (such as in federations) these can now be created with the @cypher directive instead:

type Movie @node {
    director: Person
        @cypher(
            statement: """
            MATCH (this)-[:ACTED_IN]->(p:Person)
            RETURN p
            """
            columnName: "p"
        )
}

Removed the connect overwrite argument

The overwrite argument has been removed in connect operations.

In version 7.0.0, connect operations have been simplified to always create a new relationship between nodes, regardless of whether a relationship already exists. See Connect operations now support multiple relationships between the same nodes.

If you must update an existing relationship instead of creating a new one, use the update operation:

mutation {
  updateMovies(
    update: {
      actors: [
        {
          where: { node: { name: { eq: "Keanu" } } }
          update: { edge: { role: { set: "Neo" } } }
        }
      ]
    }
  ) {
    movies {
      title
    }
  }
}

Removed support for connectOrCreate operations

The connectOrCreate operation has been removed due to limitations on its feature set when compared to other operations.

Removed aggregate fields outside connection fields

Deprecated aggregate fields have been removed from the schema. Use aggregation fields within the connection selection set instead.

Before After
query {
    usersAggregate {
        count
    }
}
query {
    usersConnection {
        aggregate {
            count {
                nodes
            }
        }
    }
}

The deprecatedAggregateOperations option has been removed from the excludeDeprecatedFields setting.

Subscriptions are now opt-in

Subscriptions are no longer automatically generated for all @node types. In version 7.x, the @subscription directive is required to enable this functionality. You must now explicitly enable subscriptions using the @subscription directive in one of two ways:

  • At the schema level to enable subscriptions for all types:

extend schema @subscription
  • At the type level to enable subscriptions only for specific types:

type Movie @node @subscription {
    title: String!
}

Removed publish method from Neo4jGraphQLSubscriptionsEngine

The publish method has been removed from the Neo4jGraphQLSubscriptionsEngine interface as it is no longer used with Change Data Capture (CDC) based subscriptions. Implementing this method on custom engines will no longer have an effect, and it is no longer possible to call publish directly on Neo4jGraphQLSubscriptionsCDCEngine.

Connect operations now support multiple relationships between the same nodes

The connect operations have been enhanced to support creating multiple relationships between the same pair of nodes. When performing a connect operation between two nodes, a new relationship is always created, even if one or more relationships of the same type already exist between those nodes. This enables modeling scenarios where multiple distinct relationships of the same type are needed between the same nodes. For example, an actor playing multiple roles in the same movie.

Changed behavior for multiple relationships between nodes

The way multiple relationships are handled between the same two nodes has changed:

  • In entity query fields (like movies), duplicate nodes are removed and only distinct results are returned, regardless of how many relationships exist between the nodes.

  • In connection query fields (like moviesConnection), all relationships are represented individually. This allows for projecting relationship properties for each connection between the two nodes.

For example, consider a scenario where Eddie Murphy played multiple roles in the same movie:

CREATE (eddie:Person {name: "Eddie Murphy"})
CREATE (nuttyProfessor:Movie { title: "The Nutty Professor" })
CREATE (eddie)-[:ACTED_IN { role: "Professor"}]->(nuttyProfessor)
CREATE (eddie)-[:ACTED_IN { role: "Buddy Love"}]->(nuttyProfessor)

With a standard entity query:

{
  actors(where: {name: {eq: "Eddie Murphy"}}) {
    name
    movies {
      title
    }
  }
}

The result shows "The Nutty Professor" only once (nodes are deduplicated):

{
  "actors": [
    {
      "name": "Eddie Murphy",
      "movies": [
        {
          "title": "The Nutty Professor"
        }
      ]
    }
  ]
}

However, with a connection query:

{
  actors(where: { name: { eq: "Eddie Murphy" } }) {
    name
    moviesConnection {
      edges {
        properties {
          role
        }
        node {
          title
        }
      }
    }
  }
}

The result preserves both relationships with their distinct properties:

{
  "actors": [
    {
      "name": "Eddie Murphy",
      "moviesConnection": {
        "edges": [
          {
            "properties": {
              "role": "Professor"
            },
            "node": {
              "title": "The Nutty Professor"
            }
          },
          {
            "properties": {
              "role": "Buddy Love"
            },
            "node": {
              "title": "The Nutty Professor"
            }
          }
        ]
      }
    }
  ]
}

Removed the @private directive

This directive was intended to be used with the library @neo4j/graphql-ogm which is no longer supported.

Schema generation avoids conflicting plural names

Schema generation now detects conflicting plural names in types and fails intentionally. For example, the following schema will fail due to ambiguous Techs plural:

type Tech @node(plural: "Techs") {
    name: String
}

type Techs @node {
    value: String
}

Full-text search changes

The @fulltext directive has been significantly changed and now requires an index name, a query name and fields to be indexed:

"""
Informs @neo4j/graphql that there should be a fulltext index in the database, allowing users to search by the index in the generated schema.
"""
directive @fulltext(
  indexes: [FulltextInput!]!
) on OBJECT

input FulltextInput {
  indexName: String!
  queryName: String!
  fields: [String!]!
}

Here is an example of how to use it:

type Movie @node @fulltext(
  indexes: [
    {
      indexName: "movieTitleIndex"
      queryName: "moviesByTitle"
      fields: ["title"]
    }
  ]
) {
  title: String!
}

Full-text search was previously available in two different locations. The following form has been completely removed:

# No longer supported
{
  movies(fulltext: { movieTitleIndex: { phrase: "The Matrix" } }) {
    title
  }
}

The root-level query has been changed to use the Relay Cursor Connections Specification:

Before After
query {
  moviesByTitle(phrase: "The Matrix") {
    score
    movies {
      title
    }
  }
}
query {
  moviesByTitle(phrase: "The Matrix") {
    edges {
      score
      node {
        title
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

The new form uses the Relay Cursor Connections Specification, which allows for pagination using cursors and access to the pageInfo field.

Removed implicit set operations

Implicit set operations in update mutations have been removed and the migration path involves two steps:

Before After
mutation {
  updateMovies(
    where: { title: { eq: "Matrix" } },
    update: { title: "The Matrix" }
  ) {
    movies {
      title
    }
  }
}
mutation {
  updateMovies(
    where: { title: { eq: "Matrix" } },
    update: { title: { set: "The Matrix" } }
  ) {
    movies {
      title
    }
  }
}

The implicitSet option of excludeDeprecatedFields has been removed.

@coalesce directive affects projection field values

In version 7.0.0, the @coalesce directive now applies to both filter operations and field projections. Previously, the fallback values specified in the @coalesce directive were only used when those fields were used in filters, but not on the returned fields. Now, when you select a field annotated with @coalesce, null values are replaced with the specified fallback value in the query result.

Consider this schema:

type Movie @node {
  title: String!
  rating: Float @coalesce(value: 5.0)
}

Previously, requesting a movie with a null rating would return:

query {
  movies {
    title
    rating  # Would return null if the rating wasn't set
  }
}
{
  "movies": [
    {
      "title": "The Matrix",
      "rating": null
    }
  ]
}

Now, the same query returns the coalesced value:

{
  "movies": [
    {
      "title": "The Matrix",
      "rating": 5.0  # Returns the fallback value specified in @coalesce
    }
  ]
}

This ensures consistent behavior between filtering and projecting fields with the @coalesce directive.

Set addVersionPrefix to true by default

In version 7.0.0, the addVersionPrefix option is set to true by default. This means that all generated Cypher queries are automatically prefixed with the Cypher version:

CYPHER 5
MATCH(this:Movie)

This ensures that the correct Cypher version is used when queries are executed in Neo4j. However, this change may be incompatible with older versions of Neo4j.

Set cypherQueryOptions.addVersionPrefix to false to disable this behavior:

{
    cypherQueryOptions: {
        addVersionPrefix: false,
    },
}

For example, when using Apollo Server:

await startStandaloneServer(server, {
    context: async ({ req }) => ({
        req,
        cypherQueryOptions: {
            addVersionPrefix: false,
        },
    }),
    listen: { port: 4000 },
});

Changed DateTime and Time value conversion behavior

In version 7.0.0, DateTime and Time values are converted from Strings to temporal types directly in the generated Cypher queries, rather than in server code using the Neo4j driver.

For example, if you have a date String in your GraphQL query:

query {
  movies(where: { releaseDate: { gt: "2023-01-15T12:30:00Z" } }) {
    title
    releaseDate
  }
}

The string value "2023-01-15T12:30:00Z" is now converted to a temporal type directly in the Cypher query:

MATCH (this:Movie)
WHERE this.releaseDate > datetime($param0)
RETURN this { .title, .releaseDate } as this

Mutation operations follow relationship directions

Mutation operations now respect the queryDirection value defined in the @relationship directive when matching existing relationships. This ensures consistent behavior between queries and mutations regarding how relationships are traversed.

 When creating new relationships, the physical direction stored in the database is still determined by the `direction` argument.
The change affects only how existing relationships are matched during mutation operations.

For example, consider the following schema:

type Movie @node {
  title: String!
  actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, queryDirection: UNDIRECTED)
}

type Person @node {
  name: String!
  movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, queryDirection: UNDIRECTED)
}

With queryDirection: UNDIRECTED, mutations now traverse existing relationships in both directions when matching nodes, regardless of the base direction value. This is consistent with how queries work with the same directive configuration. Previously, mutations would always follow the explicit direction value when matching existing relationships, which could lead to inconsistent behavior between queries and mutations.

Moved where field in nested update operations

The where field for nested update operations has been removed in favor of the where inside the nested update input.

Before After
mutation {
  updateUsers(
    where: { name: { eq: "Darrell" } }
    update: {
      posts: {
        where: { node: { title: { eq: "Version 7 Release Notes" } } }
        update: { node: { title: { set: "Version 7 Release Announcement" } } }
      }
    }
  ) {
    users {
      name
      posts {
        title
      }
    }
  }
}
mutation {
  updateUsers(
    where: { name: { eq: "Darrell" } }
    update: {
      posts: {
        update: {
          where: { node: { title: { eq: "Version 7 Release Notes" } } }
          node: { title: { set: "Version 7 Release Announcement" } }
        }
      }
    }
  ) {
    users {
      name
      posts {
        title
      }
    }
  }
}

Changed behavior for single and some filters on relationships to unions

The behavior of single and some filters when used with relationships to union types has been fixed, which represents a breaking change from the previous incorrect implementation.

Consider this schema with a union type and a relationship to it:

union Production = Movie | Series

type Actor @node {
    name: String!
    actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT)
}

Previously, when using single or some filters with unions, conditions inside these filters were evaluated separately for each concrete type in the union, requiring all of them to match. This behavior was incorrect.

query {
    actors(
        where: {
            actedIn: { single: { Movie: { title: { contains: "The Office" } }, Series: { title: { endsWith: "Office" } } } }
        }
    ) {
        name
    }
}

The fixed behavior in version 7.0.0:

  • single: Now correctly returns actors with exactly one related node across the whole union, rather than per type.

  • some: Now correctly returns actors with at least one matching related node of any type in the union.

This change provides more logical and consistent results, but may affect existing queries that relied on the previous behavior. This fix applies to both the new filter syntax and the deprecated filters (e.g., actedIn_SINGLE and actedIn_SOME).

List of nullable elements no longer supported

A list of nullable elements is no longer supported in types annotated with the @node directive, for example in the following type definition:

type Actor @node {
  name: String
  pseudonyms: [String]!
}

This is due the fact that Neo4j does not support storing null values as part of a list. To create a similar but supported type definition, change the value of the pseudonyms field to a non-nullable list: [String!]!.

Interfaces and unions disallow mixing types with and without the @node directive

Interfaces and unions can only be implemented by types that are all annotated with the @node directive or none of them.

Type definitions like the following are no longer supported because the Series type is missing the @node directive:

interface Production @node {
  title: String
}
type Movie @node implements Production {
  title: String
}
type Series implements Production {
  title: String
}

Relationship fields require @node types

The @relationship directive can only be applied to fields whose types are annotated with the @node directive.

This also applies to fields defined as interfaces and unions where they must be implemented exclusively by @node types.

Deprecations

Deprecated mutations operators outside dedicated input

The following operators are now deprecated:

  • _SET

  • _POP

  • _PUSH

  • _INCREMENT

  • _ADD

  • _DECREMENT

  • _SUBTRACT

  • _MULTIPLY

  • _DIVIDE

Use the dedicated input object versions instead:

Before After
mutation {
  updateMovies(
    where: { title: { eq: "Matrix" } },
    update: { title_SET: "The Matrix" }
  ) {
    movies {
      title
    }
  }
}
mutation {
  updateMovies(
    where: { title: { eq: "Matrix" } },
    update: { title: { set: "The Matrix" }  }
  ) {
    movies {
      title
    }
  }
}

The excludeDeprecatedFields option now contains the option mutationOperations to remove these deprecated operators:

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  excludeDeprecatedFields: {
    mutationOperations: true
  }
});

Deprecated the _EQ filter syntax

The _EQ filter syntax is now deprecated. Use the dedicated input object versions instead:

Before After
{
  movies(where: { title_EQ: "The Matrix" }) {
    title
  }
}
{
  movies(where: { title: { eq: "The Matrix" } }) {
    title
  }
}

The excludeDeprecatedFields option now contains the option attributeFilters to remove these deprecated filters:

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  excludeDeprecatedFields: {
    attributeFilters: true
  }
});

Deprecated aggregation filters outside connection fields

The aggregation filters outside connection fields are now deprecated. Use the aggregation filters within connection input fields instead:

Before After
{
    posts(where: { likesAggregate: { node: { someInt: { average: { eq: 10 } } } } }) {
        content
    }
}
{
    posts(where: { likesConnection: { aggregate: { node: { someInt: { average: { eq: 10 } } } } } }) {
        content
    }
}

The excludeDeprecatedFields option now contains the option aggregationFiltersOutsideConnection to remove these deprecated filters:

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  excludeDeprecatedFields: {
    aggregationFiltersOutsideConnection: true
  }
});

Deprecated aggregation filters outside dedicated input

Aggregation filters outside dedicated input like _AVERAGE_GT are now deprecated. Use the dedicated input object versions instead:

Before After
query Movies {
  movies(
    where: { actorsAggregate: { node: { lastRating_AVERAGE_GT: 6 } } }
  ) {
    title
  }
}
query Movies {
  movies(
    where: {
      actorsAggregate: { node: { lastRating: { average: { gt: 6 } } } }
    }
  ) {
    title
  }
}

The excludeDeprecatedFields option now contains the option aggregationFilters to remove these deprecated filters:

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  excludeDeprecatedFields: {
    aggregationFilters: true
  }
});

Deprecated relationship filters outside dedicated input

The relationship filtering syntax outside dedicated input like _SOME, _ALL, and _NONE is now deprecated. Use the dedicated input object versions instead:

Before After
{
  movies(where: { actors_SOME: { name_EQ: "Keanu Reeves" } }) {
    title
  }
}
{
  movies(where: { actors: { some: { name: { eq: "Keanu Reeves" } } } }) {
    title
  }
}

The excludeDeprecatedFields option now contains the option relationshipFilters to remove these deprecated filters:

const neoSchema = new Neo4jGraphQL({
  typeDefs,
  excludeDeprecatedFields: {
    relationshipFilters: true
  }
});