Skip to content

Commit

Permalink
Merge pull request #5819 from neo4j/remove-single-relationship
Browse files Browse the repository at this point in the history
Remove single relationships
  • Loading branch information
angrykoala authored Nov 20, 2024
2 parents e2337b2 + 4936eec commit 049d6d5
Show file tree
Hide file tree
Showing 225 changed files with 2,606 additions and 21,501 deletions.
38 changes: 38 additions & 0 deletions .changeset/little-lemons-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@neo4j/graphql": major
---

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

Before

```graphql
type Movie {
director: Person @relationship(type: "DIRECTED", direction: "IN")
}
```

After

```graphql
type Movie {
director: [Person!]! @relationship(type: "DIRECTED", direction: "IN")
}
```

This requires updating filters, clients and auth rules to use the list filter operations.

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

```graphql
type Movie {
director: Person
@cypher(
statement: """
MATCH(this)-[:ACTED_IN]->(p:Person)
RETURN p
"""
columnName: "p"
)
}
```
60 changes: 48 additions & 12 deletions packages/apollo-federation-subgraph-compatibility/src/type-defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,39 +42,75 @@ export const typeDefs = gql`
directive @custom on OBJECT
type Product @custom @key(fields: "id") @key(fields: "sku package") @key(fields: "sku variation { id }") {
type Product @custom @key(fields: "id") @key(fields: "sku package") @key(fields: "sku variation { id }") @node {
id: ID!
sku: String
package: String
variation: ProductVariation @relationship(type: "HAS_VARIATION", direction: OUT)
dimensions: ProductDimension @relationship(type: "HAS_DIMENSIONS", direction: OUT)
createdBy: User @provides(fields: "totalProductsCreated") @relationship(type: "CREATED_BY", direction: OUT)
variation: ProductVariation
@cypher(
statement: """
MATCH (this)-[:HAS_VARIATION]->(res:ProductVariation)
RETURN res
"""
columnName: "res"
)
dimensions: ProductDimension
@cypher(
statement: """
MATCH (this)-[:HAS_DIMENSIONS]->(res:ProductDimension)
RETURN res
"""
columnName: "res"
)
createdBy: User
@provides(fields: "totalProductsCreated")
@cypher(
statement: """
MATCH (this)-[:CREATED_BY]->(res:User)
RETURN res
"""
columnName: "res"
)
notes: String @tag(name: "internal")
research: [ProductResearch!]! @relationship(type: "HAS_RESEARCH", direction: OUT)
}
type DeprecatedProduct @key(fields: "sku package") {
type DeprecatedProduct @key(fields: "sku package") @node {
sku: String!
package: String!
reason: String
createdBy: User @relationship(type: "CREATED_BY", direction: OUT)
createdBy: User
@cypher(
statement: """
MATCH (this)-[:CREATED_BY]->(res:User)
RETURN res
"""
columnName: "res"
)
}
type ProductVariation {
type ProductVariation @node {
id: ID!
}
type ProductResearch @key(fields: "study { caseNumber }") {
study: CaseStudy! @relationship(type: "HAS_STUDY", direction: OUT)
type ProductResearch @key(fields: "study { caseNumber }") @node {
study: CaseStudy!
@cypher(
statement: """
MATCH (this)-[:HAS_STUDY]->(res:CaseStudy)
RETURN res
"""
columnName: "res"
)
outcome: String
}
type CaseStudy {
type CaseStudy @node {
caseNumber: ID!
description: String
}
type ProductDimension @shareable {
type ProductDimension @shareable @node {
size: String
weight: Float
unit: String @inaccessible
Expand All @@ -93,7 +129,7 @@ export const typeDefs = gql`
# Should be extend type as below
# extend type User @key(fields: "email") {
type User @key(fields: "email") @extends {
type User @key(fields: "email") @node @extends {
averageProductsCreatedPerYear: Int @requires(fields: "totalProductsCreated yearsOfEmployment")
email: ID! @external
name: String @override(from: "users")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ export function createRelationshipFields({
return;
}

if (!relationshipAdapter.isList) {
throw new Error(
`@relationship on non-list field [${relationshipAdapter.source.name}.${relationshipAdapter.name}] not supported`
);
}

// TODO: find a way to merge these 2 into 1 RelationshipProperties generation function
if (relationshipAdapter instanceof RelationshipDeclarationAdapter) {
doForRelationshipDeclaration({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,27 @@

import type { ASTVisitor, FieldDefinitionNode, ListTypeNode, NonNullTypeNode } from "graphql";
import { Kind } from "graphql";
import { relationshipDirective } from "../../../../graphql/directives";

export function WarnIfSingleRelationships(): ASTVisitor {
let warningAlreadyIssued = false;
import type { SDLValidationContext } from "graphql/validation/ValidationContext";
import { relationshipDirective } from "../../../graphql/directives";
import { createGraphQLError } from "./utils/document-validation-error";

export function ErrorIfSingleRelationships(context: SDLValidationContext): ASTVisitor {
return {
FieldDefinition(field: FieldDefinitionNode) {
if (!warningAlreadyIssued) {
let isRelationship = false;
for (const directive of field.directives ?? []) {
if (directive.name.value === relationshipDirective.name) {
isRelationship = true;
}
let isRelationship = false;
for (const directive of field.directives ?? []) {
if (directive.name.value === relationshipDirective.name) {
isRelationship = true;
}
}

const isList = Boolean(getListTypeNode(field));

if (isRelationship && !isList) {
console.warn(
"Using @relationship directive on a non-list element is deprecated and will be removed in next major version."
);
warningAlreadyIssued = true;
}
const isList = Boolean(getListTypeNode(field));
if (isRelationship && !isList) {
context.reportError(
createGraphQLError({
errorMsg: `Using @relationship directive on a non-list property "${field.name.value}" is not supported.`,
})
);
}
},
};
Expand Down
Loading

0 comments on commit 049d6d5

Please sign in to comment.