Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-insects-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neo4j/graphql": major
---

Made `@relationshipProperties` mandatory for relationship property interfaces
4 changes: 2 additions & 2 deletions docs/modules/ROOT/pages/directives.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ Reference: xref::type-definitions/relationships.adoc[Relationships]

== `@relationshipProperties`

Optional syntactic sugar to help you distinguish between interfaces which are used for relationship properties, and otherwise.
Required to help you distinguish between interfaces which are used for relationship properties, and otherwise.

Can only be used on interfaces, as per its definition:

[source, graphql, indent=0]
----
"""Syntactic sugar to help differentiate between interfaces for relationship properties, and otherwise."""
"""Required to differentiate between interfaces for relationship properties, and otherwise."""
directive @relationshipProperties on INTERFACE
----

Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/filtering.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ type Person {
name: String
}

interface ActedIn {
interface ActedIn @relationshipProperties {
screenTime: Int
}
----
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/guides/v2-migration/mutations.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[[v2-migration-mutations]]
= Mutations

The most broadly affected area of functionality by the 2.0.0 upgrade are the nested operations of Mutations, to faciliate the mutation of and filtering on relationship properties.
The most broadly affected area of functionality by the 2.0.0 upgrade are the nested operations of Mutations, to facilitate the mutation of and filtering on relationship properties.

The examples in this section will be based off the following type definitions:

Expand Down
140 changes: 91 additions & 49 deletions docs/modules/ROOT/pages/guides/v4-migration/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,75 @@ const neoSchema = new Neo4jGraphQL({
})
----

==== `requires` changes

In version 4.0.0, it is now possible to require non-scalar fields. This means it is also possible to require fields on related type.
To make this possible, the `requires` argument now accept a graphql selection set instead of a list of strings.

Therefore, the following type definitions:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: ["firstName", "lastName"])
}
----

Would need to be modified to use a selection set as below:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName lastName")
}
----

Below is a more advanced example showing what the selection set is capable of:

[source, graphql, indent=0]
----
interface Publication {
publicationYear: Int!
}

type Author {
name: String!
publications: [Publication!]! @relationship(type: "WROTE", direction: OUT)
publicationsWithAuthor: [String!]!
@customResolver(
requires: "name publications { publicationYear ...on Book { title } ... on Journal { subject } }"
)
}

type Book implements Publication {
title: String!
publicationYear: Int!
author: [Author!]! @relationship(type: "WROTE", direction: IN)
}

type Journal implements Publication {
subject: String!
publicationYear: Int!
author: [Author!]! @relationship(type: "WROTE", direction: IN)
}
----

Additionally, the requires argument also validates the required selection set against your type definitions.
Therefore, as there is no field called `someFieldThatDoesNotExist`, an error would be thrown on startup if you tried to use the following type definitions:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName someFieldThatDoesNotExist")
}
----

[plural-migration]
=== `plural` argument removed from `@node` and replaced with `@plural`

Expand Down Expand Up @@ -406,76 +475,49 @@ type query {

Additionally, escaping strings is no longer needed.

=== `@customResolver` changes
=== Mandatory `@relationshipProperties`

In version 4.0.0, it is now possible to require non-scalar fields. This means it is also possible to require fields on related type.
To make this possible, the `requires` argument now accept a graphql selection set instead of a list of strings.
Upcoming changes to interfaces require us to distinguish between interfaces that are used to specify relationship properties, and others. Therefore, the `@relationshipProperties` directive is now required on all relationship property interfaces.
If it is not included, an error will be thrown.

Therefore, the following type definitions:
As a result, in version 4.0.0, the following type definitions are invalid:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: ["firstName", "lastName"])
type Person {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}
----

Would need to be modified to use a selection set as below:
type Movie {
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName lastName")
interface ActedIn {
screenTime: Int!
}
----

Below is a more advanced example showing what the selection set is capable of:
Instead, they would need to be updated as below:

[source, graphql, indent=0]
----
interface Publication {
publicationYear: Int!
type Person {
name: String!
movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

type Author {
name: String!
publications: [Publication!]! @relationship(type: "WROTE", direction: OUT)
publicationsWithAuthor: [String!]!
@customResolver(
requires: "name publications { publicationYear ...on Book { title } ... on Journal { subject } }"
)
}

type Book implements Publication {
title: String!
publicationYear: Int!
author: [Author!]! @relationship(type: "WROTE", direction: IN)
}

type Journal implements Publication {
subject: String!
publicationYear: Int!
author: [Author!]! @relationship(type: "WROTE", direction: IN)
type Movie {
title: String!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}
----

Additionally, the requires argument also validates the required selection set against your type definitions.
Therefore, as there is no field called `someFieldThatDoesNotExist`, an error would be thrown on startup if you tried to use the following type definitions:

[source, graphql, indent=0]
----
type User {
firstName: String!
lastName: String!
fullName: String! @customResolver(requires: "firstName someFieldThatDoesNotExist")
interface ActedIn @relationshipProperties {
screenTime: Int!
}
----


== Miscellaneous changes

[[startup-validation]]
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/queries.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type User {
friends: [User!]! @relationship(type: "FRIENDS_WITH", direction: OUT)
}

interface PostedAt {
interface PostedAt @relationshipProperties {
date: DateTime
}
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ type Influencer implements Reviewer {
reputation: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ type Influencer implements Reviewer {
reputation: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/subscriptions/filtering.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ type Magazine implements Reviewer {
reputation: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}
----
Expand Down
3 changes: 2 additions & 1 deletion docs/modules/ROOT/pages/type-definitions/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ Note there is a directive on each "end" of the relationship in this case, but it

=== Relationship properties

In order to add relationship properties to a relationship, you need to add a new type to your type definitions, but this time it will be of type `interface`. For example, for your "ACTED_IN" relationship, add a property "roles":
In order to add relationship properties to a relationship, you need to add a new type to your type definitions, but this time it will be of type `interface`. This interface must be decorated with the `@relationshipProperties` directive.
For example, for your "ACTED_IN" relationship, add a property "roles":

[source, graphql, indent=0]
----
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/type-definitions/interfaces.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The Neo4j GraphQL Library supports the use of interfaces on relationship fields.

== Type definitions

The following schema defines a `Actor` type, that has a relationship `ACTED_IN`, of type `[Production!]!`. `Production` is an interface type with `Movie` and `Series` implementations. Note in this example that relationship properties have also been used with the `@relationshipProperties` directive as syntactic sugar, so that interfaces representing relationship properties can be easily distinguished.
The following schema defines a `Actor` type, that has a relationship `ACTED_IN`, of type `[Production!]!`. `Production` is an interface type with `Movie` and `Series` implementations. Note in this example that relationship properties have also been used with the `@relationshipProperties` directive, so that interfaces representing relationship properties can be easily distinguished.

[source, graphql, indent=0]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The following should be noted about the fields you just added:

Relationship properties can be added to the above type definitions in two steps:

1. Add an interface definition containing the desired relationship properties
1. Add an interface definition, decorated with the `@relationshipProperties` directive, containing the desired relationship properties
2. Add a `properties` argument to both "sides" of the `@relationship` directive which points to the newly defined interface

For example, to distinguish which roles an actor played in a movie:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ import { DirectiveLocation, GraphQLDirective } from "graphql";

export const relationshipPropertiesDirective = new GraphQLDirective({
name: "relationshipProperties",
description: "Syntactic sugar to help differentiate between interfaces for relationship properties, and otherwise.",
description: "Required to differentiate between interfaces for relationship properties, and otherwise.",
locations: [DirectiveLocation.INTERFACE],
});
8 changes: 8 additions & 0 deletions packages/graphql/src/schema/get-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ function getNodes(
`Cannot find interface specified in ${definition.name.value}.${relationship.fieldName}`
);
}
const relationshipPropertiesDirective = propertiesInterface.directives?.find(
(directive) => directive.name.value === "relationshipProperties"
);
if (!relationshipPropertiesDirective) {
throw new Error(
`The \`@relationshipProperties\` directive could not be found on the \`${relationship.properties}\` interface`
);
}
relationshipPropertyInterfaceNames.add(relationship.properties);
}
if (relationship.interface) {
Expand Down
14 changes: 7 additions & 7 deletions packages/graphql/src/schema/make-augmented-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ describe("makeAugmentedSchema", () => {
name: String
}

interface ActedIn @auth(rules: [{ operations: [CREATE], roles: ["admin"] }]) {
interface ActedIn @auth(rules: [{ operations: [CREATE], roles: ["admin"] }]) @relationshipProperties {
screenTime: Int
}
`;
Expand All @@ -228,7 +228,7 @@ describe("makeAugmentedSchema", () => {
name: String
}

interface ActedIn {
interface ActedIn @relationshipProperties {
screenTime: Int @auth(rules: [{ operations: [CREATE], roles: ["admin"] }])
}
`;
Expand All @@ -246,7 +246,7 @@ describe("makeAugmentedSchema", () => {
name: String
}

interface ActedIn {
interface ActedIn @relationshipProperties {
actors: Actor! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}
`;
Expand All @@ -266,7 +266,7 @@ describe("makeAugmentedSchema", () => {
name: String
}

interface ActedIn {
interface ActedIn @relationshipProperties {
id: ID @cypher(statement: "RETURN id(this) as id", columnName: "id")
roles: [String]
}
Expand All @@ -285,7 +285,7 @@ describe("makeAugmentedSchema", () => {
actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

interface ActedIn {
interface ActedIn @relationshipProperties {
node: ID
}

Expand All @@ -306,7 +306,7 @@ describe("makeAugmentedSchema", () => {
actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn")
}

interface ActedIn {
interface ActedIn @relationshipProperties {
cursor: ID
}

Expand Down Expand Up @@ -334,7 +334,7 @@ describe("makeAugmentedSchema", () => {
name: String
}

interface ActedIn {
interface ActedIn @relationshipProperties {
id: ID @unique
roles: [String]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ describe("Subscription authentication", () => {
year: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("Create Relationship Subscription", () => {
year: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("Delete Subscriptions - with interfaces, unions and nested operations",
year: Int!
}

interface Review {
interface Review @relationshipProperties {
score: Int!
}

Expand Down
Loading