diff --git a/modules/ROOT/pages/migration/index.adoc b/modules/ROOT/pages/migration/index.adoc index edcd0b51..d861ac8c 100644 --- a/modules/ROOT/pages/migration/index.adoc +++ b/modules/ROOT/pages/migration/index.adoc @@ -16,25 +16,52 @@ 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: +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: + +[cols="1,1"] +|=== +| Before | After +a| [source, graphql, indent=0] ---- -type Movie { - title: String! @unique() +{ + movies(where: { title: "The Matrix" }) { + title + } } ---- +a| +[source, graphql, indent=0] +---- +{ + movies(where: { title: { eq: "The Matrix" } }) { + title + } +} +---- +|=== + +The `implicitEqualFilters` option of `excludeDeprecatedFields` has been removed. -=== Deprecated single element relationships +=== Required `@node` directive -Single element relationships have been deprecated in favor of list relationships: +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. [cols="1,1"] |=== @@ -44,32 +71,1035 @@ a| [source, graphql, indent=0] ---- type Movie { + title: String! +} +---- +a| +[source, graphql, indent=0] +---- +type Movie @node { + title: String! +} +---- +|=== + + +=== Removed the deprecated `options` argument + +The deprecated `options` argument has been removed. + +Consider the following type definition: + +[source, graphql, indent=0] +---- +type Movie @node { + title: String! +} +---- + +The following shows the difference for options: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +{ + movies(options: { first: 10, offset: 10, sort: [{ title: ASC }] }) { + title + } +} +---- +a| +[source, graphql, indent=0] +---- +{ + 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. + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +{ + productions(where: { typename_IN: ["Movie", "Series"] }) { + title + } +} +---- +a| +[source, graphql, indent=0] +---- +{ + 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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +type Movie @node { director: Person @relationship(type: "DIRECTED", direction: "IN") } ---- a| [source, graphql, indent=0] ---- -type Movie { +type Movie @node { director: [Person!]! @relationship(type: "DIRECTED", direction: "IN") } ---- |=== -Single element relationships cannot be reliably enforced, leading to a data inconsistent with the schema. +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: +[source, graphql, indent=0] +---- +type Movie @node { + director: Person + @cypher( + statement: """ + MATCH (this)-[:ACTED_IN]->(p:Person) + RETURN p + """ + columnName: "p" + ) +} +---- -=== `overwrite` +=== Removed the connect `overwrite` argument +The `overwrite` argument has been removed in connect operations. -The `overwrite` argument in `connect` operations has been deprecated. -This is part of the overarching changes to how `connect` operations work in 7.x. +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 xref:#connect-operation-details[]. -Instead, use the `update` operation to update a relationship. +If you must update an existing relationship instead of creating a new one, use the `update` operation: +[source, graphql, indent=0] +---- +mutation { + updateMovies( + update: { + actors: [ + { + where: { node: { name: { eq: "Keanu" } } } + update: { edge: { role: { set: "Neo" } } } + } + ] + } + ) { + movies { + title + } + } +} +---- -=== Removed the `connectOrCreate` operation +=== Removed support for `connectOrCreate` operations The `connectOrCreate` operation has been removed due to limitations on its feature set when compared to other operations. -`connectOrCreate` relies on `MERGE` operations in Cypher, which did not allow for `onCreate` operations to follow nested relationships. +=== Removed aggregate fields outside connection fields + +Deprecated aggregate fields have been removed from the schema. +Use aggregation fields within the connection selection set instead. + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +query { + usersAggregate { + count + } +} +---- +a| +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +extend schema @subscription +---- + +- At the type level to enable subscriptions only for specific types: + +[source, graphql, indent=0] +---- +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-operation-details]] +=== 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: + +[source, cypher, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +{ + actors(where: {name: {eq: "Eddie Murphy"}}) { + name + movies { + title + } + } +} +---- + +The result shows "The Nutty Professor" only once (nodes are deduplicated): + +[source, json, indent=0] +---- +{ + "actors": [ + { + "name": "Eddie Murphy", + "movies": [ + { + "title": "The Nutty Professor" + } + ] + } + ] +} +---- + +However, with a connection query: + +[source, graphql, indent=0] +---- +{ + actors(where: { name: { eq: "Eddie Murphy" } }) { + name + moviesConnection { + edges { + properties { + role + } + node { + title + } + } + } + } +} +---- + +The result preserves both relationships with their distinct properties: + +[source, json, indent=0] +---- +{ + "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: + +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +""" +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: + +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +# No longer supported +{ + movies(fulltext: { movieTitleIndex: { phrase: "The Matrix" } }) { + title + } +} +---- + +The root-level query has been changed to use the https://relay.dev/graphql/connections.htm[Relay Cursor Connections Specification]: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +query { + moviesByTitle(phrase: "The Matrix") { + score + movies { + title + } + } +} +---- +a| +[source, graphql, indent=0] +---- +query { + moviesByTitle(phrase: "The Matrix") { + edges { + score + node { + title + } + } + pageInfo { + hasNextPage + endCursor + } + } +} +---- +|=== + +The new form uses the https://relay.dev/graphql/connections.htm[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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +mutation { + updateMovies( + where: { title: { eq: "Matrix" } }, + update: { title: "The Matrix" } + ) { + movies { + title + } + } +} +---- +a| +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +type Movie @node { + title: String! + rating: Float @coalesce(value: 5.0) +} +---- + +Previously, requesting a movie with a null rating would return: + +[source, graphql, indent=0] +---- +query { + movies { + title + rating # Would return null if the rating wasn't set + } +} +---- + +[source, json, indent=0] +---- +{ + "movies": [ + { + "title": "The Matrix", + "rating": null + } + ] +} +---- + +Now, the same query returns the coalesced value: + +[source, json, indent=0] +---- +{ + "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: + +[source, cypher, indent=0] +---- + +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: + +[source, javascript, indent=0] +---- +{ + cypherQueryOptions: { + addVersionPrefix: false, + }, +} +---- + +For example, when using Apollo Server: + +[source, javascript, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +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: + +[source, cypher, indent=0] +---- +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. + +[NOTE] +==== + 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: + +[source, graphql, indent=0] +---- +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. + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +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 + } + } + } +} +---- +a| +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +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. + +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +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: + +[source, graphql, indent=0] +---- +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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +mutation { + updateMovies( + where: { title: { eq: "Matrix" } }, + update: { title_SET: "The Matrix" } + ) { + movies { + title + } + } +} +---- +a| +[source, graphql, indent=0] +---- +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: + +[source, javascript, indent=0] +---- +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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +{ + movies(where: { title_EQ: "The Matrix" }) { + title + } +} +---- +a| +[source, graphql, indent=0] +---- +{ + movies(where: { title: { eq: "The Matrix" } }) { + title + } +} +---- +|=== + +The `excludeDeprecatedFields` option now contains the option `attributeFilters` to remove these deprecated filters: + +[source, javascript, indent=0] +---- +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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +{ + posts(where: { likesAggregate: { node: { someInt: { average: { eq: 10 } } } } }) { + content + } +} +---- +a| +[source, graphql, indent=0] +---- +{ + posts(where: { likesConnection: { aggregate: { node: { someInt: { average: { eq: 10 } } } } } }) { + content + } +} +---- +|=== + +The `excludeDeprecatedFields` option now contains the option `aggregationFiltersOutsideConnection` to remove these deprecated filters: + +[source, javascript, indent=0] +---- +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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +query Movies { + movies( + where: { actorsAggregate: { node: { lastRating_AVERAGE_GT: 6 } } } + ) { + title + } +} +---- +a| +[source, graphql, indent=0] +---- +query Movies { + movies( + where: { + actorsAggregate: { node: { lastRating: { average: { gt: 6 } } } } + } + ) { + title + } +} +---- +|=== + +The `excludeDeprecatedFields` option now contains the option `aggregationFilters` to remove these deprecated filters: + +[source, javascript, indent=0] +---- +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: + +[cols="1,1"] +|=== +| Before | After + +a| +[source, graphql, indent=0] +---- +{ + movies(where: { actors_SOME: { name_EQ: "Keanu Reeves" } }) { + title + } +} +---- +a| +[source, graphql, indent=0] +---- +{ + movies(where: { actors: { some: { name: { eq: "Keanu Reeves" } } } }) { + title + } +} +---- +|=== + +The `excludeDeprecatedFields` option now contains the option `relationshipFilters` to remove these deprecated filters: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + excludeDeprecatedFields: { + relationshipFilters: true + } +}); +---- \ No newline at end of file diff --git a/modules/ROOT/partials/notes.adoc b/modules/ROOT/partials/notes.adoc index 501c2534..d9ab3c27 100644 --- a/modules/ROOT/partials/notes.adoc +++ b/modules/ROOT/partials/notes.adoc @@ -1,7 +1,7 @@ # tag::note[] [NOTE] ==== -This is the documentation of the GraphQL Library version 6. +This is the documentation of the GraphQL Library version 7. For the long-term support (LTS) version 5, refer to link:{neo4j-docs-base-uri}/graphql/5/[GraphQL Library version 5 LTS]. ==== # end::note[]