4.0.0 Migration

Version 4.0.0 of the library has not yet been released. However, we recommend making these changes early in order to avoid issues in the future.

This document lists all breaking changes from version 3.x.y to 4.0.0 and how to update.

How to upgrade

Simply update @neo4j/graphql using npm or your package manager of choice:

npm update @neo4j/graphql

Updated Directives

We have renamed a number of directives and their arguments, in order to make using @neo4j/graphql more intuitive.

@callback renamed to @populatedBy

Previously, there was ambiguity over the behaviour of @callback. As the directive is used to populate a value on input, it has been renamed @populatedBy to reflect this. Additionally, the name argument was previously used to specify the callback used to populate the field’s value. This has been renamed to callback to make it clear that it refers to a callback.

Therefore, the following usage of the directive would be invalid:

type User {
  id: ID! @callback(name: "nanoid", operations: [CREATE])
  firstName: String!
  surname: String!
}

It would instead need to be updated to use the new directive and argument as below:

type User {
  id: ID! @populatedBy(callback: "nanoid", operations: [CREATE])
  firstName: String!
  surname: String!
}

Note that before and after these changes, a callback named nanoid would need to be defined as below:

new Neo4jGraphQL({
  typeDefs,
  config: {
    callbacks: {
      nanoid: () => { return nanoid(); }
    }
  }
});

@computed renamed to @customResolver

Previously, there was ambiguity over the behaviour of @computed and it wasn’t clear that it was intended to be used with a custom resolver. In order to make this clear, @computed has been renamed to @customResolver. Furthermore, the behaviour of the from argument was not clear. The argument is used to specify which fields other fields are required by the custom resolver. As a result, from has been renamed to requires.

These changes mean that the following type definition is invalid in version 4.0.0:

type User {
  firstName: String!
  lastName: String!
  fullName: String! @computed(from: ["firstName", "lastName"])
}

Instead, it would need to be updated to use the new directive and argument as below:

type User {
  firstName: String!
  lastName: String!
  fullName: String! @customResolver(requires: ["firstName", "lastName"])
}

Note that before and after these changes, a custom resolver would need to be defined as below:

new Neo4jGraphQL({
  typeDefs,
  resolvers: {
    User: {
      fullName: ({ firstName, lastName }, args, context, info) => (`${firstName} ${lastName}`),
    }
  }
});

Checks for custom resolvers

Previously, if no custom resolver was specified for a @computed field when creating an instance of Neo4jGraphQL, no errors would be thrown when generating the schema. However, it is likely that the lack of a custom resolver would lead to errors at runtime. It is preferable to fail fast in this case as it is easier to debug and makes it less likely that bugs will make it into production.

As a result, checks are now performed to ensure that every @customResolver field has a custom resolver provided. If not the library will throw an error during schema generation.

plural argument removed from @node and replaced with @plural

How a type name is pluralised has nothing to do with nodes in the database. As a result, having a plural argument on the @node directive did not make sense. As a result, the plural argument of @node has been removed and replaced with a new @plural directive. The @plural directive takes the pluralised type name using the value argument.

This means that the following type definition is invalid:

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

It would need to be updated to use the new directive as below:

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

@fulltext changes

In version 4.0.0, a number of improvements have been made to full-text queries. These include the ability to return the full-text score, filter by the score and sorting by the score.

However, these improvements required a number of breaking changes.

Query changes

Full-text queries now need to be performed using a top-level query, instead of being performed using an argument on a node query.

As a result, the following query is now invalid:

query {
  movies(fulltext: { movieTitleIndex: { phrase: "Some Title" } }) {
    title
  }
}

The new top-level queries can be used to return the full-text score, which indicates the confidence of a match, as well as the nodes that have been matched.

The new top-level queries accept the following arguments:
  • phrase which specifies the string to search for in the full-text index.

  • where which accepts a min/max score as well as the normal filters available on a node.

  • sort which can be used to sort using the score and node attributes.

  • limit which is used to limit the number of results to the given integer.

  • offset which is used to offset by the given number of results.

The new top-level queries means that for the following type definition:

type Movie @fulltext(indexes: [{ indexName: "MovieTitle", fields: ["title"] }]) { # Note that indexName is the new name for the name argument. More about this below.
  title: String!
}

The following top-level query and type definitions would be generated by the library:

type Query {
  movieFulltextMovieTitle(phrase: String!, where: MovieFulltextWhere, sort: [MovieFulltextSort!], limit: Int, offset: Int): [MovieFulltextResult!]!
}

"""The result of a fulltext search on an index of Movie"""
type MovieFulltextResult {
  score: Float
  movies: Movie
}

"""The input for filtering a fulltext query on an index of Movie"""
input MovieFulltextWhere {
  score: FloatWhere
  movie: MovieWhere
}

"""The input for sorting a fulltext query on an index of Movie"""
input MovieFulltextSort {
  score: SortDirection
  movie: MovieSort
}

"""The input for filtering the score of a fulltext search"""
input FloatWhere {
  min: Float
  max: Float
}

This query can be used to perform a full-text query as below:

query {
  movieFulltextMovieTitle(
    phrase: "Full Metal Jacket",
    where: { score: min: 0.4 },
    sort: [{ movie: { title: ASC } }],
    limit: 5,
    offset: 10
  ) {
    score
    movies {
      title
    }
  }
}

The above query would be expected to return results in the following format:

{
  "data": {
    "movieFulltextMovieTitle": [
      {
        "score": 0.44524085521698,
        "movie": {
          "title": "Full Moon High"
        }
      },
      {
        "score": 1.411118507385254,
        "movie": {
          "title": "Full Metal Jacket"
        }
      }
    ]
  }
}

Argument changes

The following changes have been made to @fulltext arguments:
  • queryName has been added to specify a custom name for the top-level query that is generated.

  • name has been renamed to indexName to avoid ambiguity with the new queryName argument.

These changes means that the following type definition is now invalid:

type Movie @fulltext(indexes: [{ name: "MovieTitle", fields: ["title"] }]) {
  title: String!
}

The name argument would need to be replaced with indexName as below:

type Movie @fulltext(indexes: [{ indexName: "MovieTitle", fields: ["title"] }]) {
  title: String!
}

The queryName argument can be used as below:

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

This means the top-level query would now be moviesByTitle instead of movieFulltextMovieTitle:

type Query {
  moviesByTitle(phrase: String!, where: MovieFulltextWhere, sort: [MovieFulltextSort!], limit: Int, offset: Int): [MovieFulltextResult!]!
}