diff --git a/.changeset/large-adults-glow.md b/.changeset/large-adults-glow.md new file mode 100644 index 0000000000..90c612a75f --- /dev/null +++ b/.changeset/large-adults-glow.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Add support for `@cypher` directive in relationship properties diff --git a/packages/graphql/src/schema/validation/custom-rules/directives/cypher.ts b/packages/graphql/src/schema/validation/custom-rules/directives/cypher.ts index 5c1ea7c88d..1b49cf2a3b 100644 --- a/packages/graphql/src/schema/validation/custom-rules/directives/cypher.ts +++ b/packages/graphql/src/schema/validation/custom-rules/directives/cypher.ts @@ -17,11 +17,12 @@ * limitations under the License. */ -import type { ASTVisitor, FieldDefinitionNode } from "graphql"; +import type { ASTNode, ASTVisitor, FieldDefinitionNode } from "graphql"; import { cypherDirective } from "../../../../graphql/directives"; -import type { Neo4jValidationContext } from "../../Neo4jValidationContext"; +import type { Neo4jValidationContext, TypeMapWithExtensions } from "../../Neo4jValidationContext"; import { assertValid, createGraphQLError, DocumentValidationError } from "../utils/document-validation-error"; import { fieldIsInNodeType } from "../utils/location-helpers/is-in-node-type"; +import { fieldIsInRelationshipPropertiesType } from "../utils/location-helpers/is-in-relationship-properties-type"; import { fieldIsInRootType } from "../utils/location-helpers/is-in-root-type"; import { fieldIsInSubscriptionType } from "../utils/location-helpers/is-in-subscription-type"; import { getPathToNode } from "../utils/path-parser"; @@ -39,10 +40,8 @@ export function validateCypherDirective(context: Neo4jValidationContext): ASTVis ) { return; } - const isValidLocation = - (fieldIsInNodeType({ path, ancestors, typeMapWithExtensions }) || - fieldIsInRootType({ path, ancestors, typeMapWithExtensions })) && - !fieldIsInSubscriptionType({ path, ancestors, typeMapWithExtensions }); + + const isValidLocation = isCypherLocationValid({ path, ancestors, typeMapWithExtensions }); const { isValid, errorMsg } = assertValid(() => { if (!isValidLocation) { @@ -66,3 +65,19 @@ export function validateCypherDirective(context: Neo4jValidationContext): ASTVis }, }; } + +function isCypherLocationValid(directiveLocationData: { + path: readonly (string | number)[]; + ancestors: readonly (ASTNode | readonly ASTNode[])[]; + typeMapWithExtensions: TypeMapWithExtensions; +}): boolean { + if (fieldIsInSubscriptionType(directiveLocationData)) { + return false; + } + + return ( + fieldIsInNodeType(directiveLocationData) || + fieldIsInRootType(directiveLocationData) || + fieldIsInRelationshipPropertiesType(directiveLocationData) + ); +} diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index f70f3f635d..47c3ef3d97 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -5240,46 +5240,6 @@ describe("validation 2.0", () => { expect(errors[0]).toHaveProperty("path", ["ActedIn", "actors", "@relationship"]); }); - test("should throw error if @cypher is used on relationship property", () => { - const relationshipProperties = gql` - type ActedIn @relationshipProperties { - id: ID @cypher(statement: "RETURN id(this) as id", columnName: "id") - roles: [String!] - } - `; - const doc = gql` - ${relationshipProperties} - type Movie @node { - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - type Actor @node { - name: String - } - `; - - const enums = [] as EnumTypeDefinitionNode[]; - const interfaces = [] as InterfaceTypeDefinitionNode[]; - const unions = [] as UnionTypeDefinitionNode[]; - const objects = relationshipProperties.definitions as ObjectTypeDefinitionNode[]; - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions: { enums, interfaces, unions, objects }, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - 'Directive "cypher" must be in a type with "@node" or on root types: Query, and Mutation' - ); - expect(errors[0]).toHaveProperty("path", ["ActedIn", "id", "@cypher"]); - }); - test("@relationshipProperties reserved field name", () => { const relationshipProperties = gql` type HasPost @relationshipProperties { @@ -5318,51 +5278,6 @@ describe("validation 2.0", () => { ); expect(errors[0]).toHaveProperty("path", ["HasPost", "cursor"]); }); - - test("@cypher forbidden on @relationshipProperties field", () => { - const relationshipProperties = gql` - type HasPost @relationshipProperties { - review: Float - @cypher( - statement: """ - WITH 2 as x RETURN x - """ - columnName: "x" - ) - } - `; - const doc = gql` - ${relationshipProperties} - type User @node { - name: String - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT, properties: "HasPost") - } - type Post @node { - title: String - } - `; - - const enums = [] as EnumTypeDefinitionNode[]; - const interfaces = [] as InterfaceTypeDefinitionNode[]; - const unions = [] as UnionTypeDefinitionNode[]; - const objects = relationshipProperties.definitions as ObjectTypeDefinitionNode[]; - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions: { enums, interfaces, unions, objects }, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - 'Directive "cypher" must be in a type with "@node" or on root types: Query, and Mutation' - ); - expect(errors[0]).toHaveProperty("path", ["HasPost", "review", "@cypher"]); - }); }); describe("valid", () => { diff --git a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts index d6b9e6eff0..bea570a959 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts @@ -21,6 +21,7 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { filterTruthy } from "../../../../utils/utils"; import { hasTarget } from "../../utils/context-has-target"; import { getEntityLabels } from "../../utils/create-node-from-entity"; import { isConcreteEntity } from "../../utils/is-concrete-entity"; @@ -206,7 +207,7 @@ export class ConnectionFilter extends Filter { const subqueries = this.innerFilters.flatMap((f) => { const nestedSubqueries = f .getSubqueries(queryASTContext) - .map((sq) => new Cypher.Call(sq, [queryASTContext.target])); + .map((sq) => new Cypher.Call(sq, filterTruthy([queryASTContext.target, queryASTContext.relationship]))); const selection = f.getSelection(queryASTContext); const predicate = f.getPredicate(queryASTContext); const clauses = [...selection, ...nestedSubqueries]; @@ -217,7 +218,6 @@ export class ConnectionFilter extends Filter { return clauses; }); - if (subqueries.length === 0) return []; // Hack logic to change predicates logic const comparisonValue = this.operator === "NONE" ? Cypher.false : Cypher.true; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts index fe292dc683..eed5ce748a 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts @@ -170,11 +170,11 @@ export class ConnectionReadOperation extends Operation { } const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => { - return new Cypher.Call(sq, [nestedContext.target]); + return new Cypher.Call(sq, filterTruthy([nestedContext.target, nestedContext.relationship])); }); const normalFilterSubqueries = this.getFilterSubqueries(nestedContext).map((sq) => { - return new Cypher.Call(sq, [nestedContext.target]); + return new Cypher.Call(sq, filterTruthy([nestedContext.target, nestedContext.relationship])); }); const filtersSubqueries = [...authFilterSubqueries, ...normalFilterSubqueries]; @@ -460,6 +460,7 @@ export class ConnectionReadOperation extends Operation { throw new Error("No parent node found!"); } const sortNodeFields = this.sortFields.flatMap((sf) => sf.node); + const sortEdgeFields = this.sortFields.flatMap((sf) => sf.edge); /** * cypherSortFieldsFlagMap is a Record that holds the name of the sort field as key * and a boolean flag defined as true when the field is a `@cypher` field. @@ -473,8 +474,17 @@ export class ConnectionReadOperation extends Operation { }, {} ); + const cypherSortFieldsEdgeFlagMap = sortEdgeFields.reduce>( + (sortFieldsFlagMap, sortField) => { + if (sortField instanceof CypherPropertySort) { + sortFieldsFlagMap[sortField.getFieldName()] = true; + } + return sortFieldsFlagMap; + }, + {} + ); - const preAndPostFields = this.nodeFields.reduce>( + const preAndPostFields = [...this.nodeFields].reduce>( (acc, nodeField) => { if ( nodeField instanceof OperationField && @@ -493,13 +503,49 @@ export class ConnectionReadOperation extends Operation { }, { Pre: [], Post: [] } ); + + const preAndPostEdgeFields = [...this.edgeFields].reduce>( + (acc, edgeField) => { + if ( + edgeField instanceof OperationField && + edgeField.isCypherField() && + edgeField.operation instanceof CypherAttributeOperation + ) { + const cypherFieldName = edgeField.operation.cypherAttributeField.name; + if (cypherSortFieldsEdgeFlagMap[cypherFieldName]) { + acc.Pre.push(edgeField); + return acc; + } + } + + acc.Post.push(edgeField); + return acc; + }, + { Pre: [], Post: [] } + ); const preNodeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostFields.Pre, [context.target]); const postNodeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostFields.Post, [context.target]); + + let preEdgeSubqueries: Cypher.Clause[] = []; + let postEdgeSubqueries: Cypher.Clause[] = []; + let sortEdgeSubqueries: Cypher.Clause[] = []; + if (context.relationship) { + preEdgeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostEdgeFields.Pre, [context.relationship]); + postEdgeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostEdgeFields.Post, [ + context.relationship, + ]); + sortEdgeSubqueries = wrapSubqueriesInCypherCalls(context, sortEdgeFields, [context.relationship]); + } const sortSubqueries = wrapSubqueriesInCypherCalls(context, sortNodeFields, [context.target]); return { - prePaginationSubqueries: [...sortSubqueries, ...preNodeSubqueries], - postPaginationSubqueries: postNodeSubqueries, + prePaginationSubqueries: [ + ...sortSubqueries, + ...sortEdgeSubqueries, + ...preNodeSubqueries, + ...preEdgeSubqueries, + ], + postPaginationSubqueries: [...postNodeSubqueries, ...postEdgeSubqueries], }; } } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts index 2f0216d46d..3152a3a8e8 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts @@ -57,6 +57,13 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { const nodeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.nodeFields, [ nestedContext.target, ]); + + let edgeProjectionSubqueries: Array = []; + if (nestedContext.relationship) { + edgeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.edgeFields, [ + nestedContext.relationship, + ]); + } const nodeProjectionMap = new Cypher.Map(); // This bit is different than normal connection ops @@ -131,6 +138,7 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { withWhere, ...validations, ...nodeProjectionSubqueries, + ...edgeProjectionSubqueries, projectionClauses ); diff --git a/packages/graphql/src/translate/queryAST/ast/selection/CustomCypherSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/CustomCypherSelection.ts index d4315953be..c84d85f6b7 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/CustomCypherSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/CustomCypherSelection.ts @@ -33,15 +33,21 @@ export class CustomCypherSelection extends EntitySelection { private rawArguments: Record; private cypherAnnotation: CypherAnnotation; private isNested: boolean; + private attachedTo: "node" | "relationship"; + /** + * @param targetRelationship - Should this selector use the relationship variable of the context as "this" target in the Cypher? (use it for edge props) + */ constructor({ operationField, rawArguments = {}, isNested, + attachedTo = "node", }: { operationField: AttributeAdapter; rawArguments: Record; isNested: boolean; + attachedTo?: "node" | "relationship"; }) { super(); this.operationField = operationField; @@ -51,6 +57,7 @@ export class CustomCypherSelection extends EntitySelection { throw new Error("Missing Cypher Annotation on Cypher field"); } this.cypherAnnotation = this.operationField.annotations.cypher; + this.attachedTo = attachedTo; } public apply(context: QueryASTContext): { @@ -80,10 +87,11 @@ export class CustomCypherSelection extends EntitySelection { let statementSubquery: Cypher.Call; - if (this.isNested && context.target) { - const aliasTargetToPublicTarget = new Cypher.With([context.target, CYPHER_TARGET_VARIABLE]); + const nestedTarget = this.attachedTo === "relationship" ? context.relationship : context.target; + if (this.isNested && nestedTarget) { + const aliasTargetToPublicTarget = new Cypher.With([nestedTarget, CYPHER_TARGET_VARIABLE]); statementSubquery = new Cypher.Call(Cypher.utils.concat(aliasTargetToPublicTarget, statementCypherQuery), [ - context.target, + nestedTarget, ]); } else { statementSubquery = new Cypher.Call(statementCypherQuery); diff --git a/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts b/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts index 112e44edb5..e3da091bb6 100644 --- a/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts +++ b/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts @@ -46,7 +46,7 @@ export class CypherPropertySort extends Sort { } public getChildren(): QueryASTNode[] { - return []; + return [this.cypherOperation]; } public print(): string { diff --git a/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts b/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts index acdb55bf20..1b2e0bedb5 100644 --- a/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts @@ -24,7 +24,7 @@ import type { AttributeAdapter } from "../../../schema-model/attribute/model-ada import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import type { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { getEntityAdapter } from "../../../schema-model/utils/get-entity-adapter"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { deepMerge } from "../../../utils/deep-merge"; @@ -240,6 +240,7 @@ export class FieldFactory { const cypherAnnotation = attribute.annotations.cypher; if (cypherAnnotation) { return this.createCypherAttributeField({ + entity, field, attribute, context, @@ -258,11 +259,13 @@ export class FieldFactory { } private createCypherAttributeField({ + entity, field, attribute, context, cypherAnnotation, }: { + entity: ConcreteEntityAdapter | RelationshipAdapter; attribute: AttributeAdapter; field: ResolveTree; context: Neo4jGraphQLTranslationContext; @@ -278,9 +281,10 @@ export class FieldFactory { // move the user specified arguments in a different object as they should be treated as arguments of a Cypher Field const cypherArguments = { ...field.args }; field.args = {}; - + const isEdge = entity instanceof RelationshipAdapter; if (rawFields) { if (attribute.typeHelper.isObject()) { + // NOTE: This entity is the cypher result type (if an entity), not the target node. Naming may be confusing const concreteEntity = this.queryASTFactory.schemaModel.getConcreteEntityAdapter(typeName); return this.createCypherOperationField({ @@ -289,6 +293,7 @@ export class FieldFactory { context, cypherAttributeField: attribute, cypherArguments, + isEdge, }); } else if (attribute.typeHelper.isAbstract()) { const entity = this.queryASTFactory.schemaModel.getEntity(typeName); @@ -300,6 +305,7 @@ export class FieldFactory { context, cypherAttributeField: attribute, cypherArguments, + isEdge, }); } } @@ -309,6 +315,7 @@ export class FieldFactory { context, cypherAttributeField: attribute, cypherArguments, + isEdge, }); } @@ -353,12 +360,14 @@ export class FieldFactory { context, cypherAttributeField, cypherArguments, + isEdge, }: { target?: EntityAdapter; field: ResolveTree; context: Neo4jGraphQLTranslationContext; cypherAttributeField: AttributeAdapter; cypherArguments?: Record; + isEdge: boolean; }): OperationField { const cypherOp = this.queryASTFactory.operationsFactory.createCustomCypherOperation({ resolveTree: field, @@ -366,6 +375,7 @@ export class FieldFactory { entity: target, cypherAttributeField, cypherArguments, + isEdge, }); return new OperationField({ diff --git a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts index 79d342ba36..9ec78eaa23 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -181,16 +181,19 @@ export class FilterFactory { comparisonValue, operator, caseInsensitive, + attachedTo, }: { attribute: AttributeAdapter; comparisonValue: GraphQLWhereArg; operator: FilterOperator | undefined; caseInsensitive?: boolean; + attachedTo?: "node" | "relationship"; }): Filter | Filter[] { const selection = new CustomCypherSelection({ operationField: attribute, rawArguments: {}, isNested: true, + attachedTo, }); if (attribute.annotations.cypher?.targetEntity) { @@ -255,6 +258,7 @@ export class FilterFactory { comparisonValue, operator, caseInsensitive, + attachedTo, }); } // Implicit _EQ filters are removed but the argument "operator" can still be undefined in some cases, for instance: diff --git a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts index 1d8cb90d58..791144167f 100644 --- a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts @@ -397,6 +397,7 @@ export class OperationsFactory { entity?: EntityAdapter; cypherAttributeField: AttributeAdapter; cypherArguments?: Record; + isEdge: boolean; }): CypherEntityOperation | CompositeCypherOperation | CypherAttributeOperation { return this.customCypherFactory.createCustomCypherOperation(arg); } diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/CustomCypherFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/CustomCypherFactory.ts index 08897ab1b7..8d871699f6 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/CustomCypherFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/CustomCypherFactory.ts @@ -44,17 +44,20 @@ export class CustomCypherFactory { entity, cypherAttributeField, cypherArguments = {}, + isEdge, }: { resolveTree?: ResolveTree; context: Neo4jGraphQLTranslationContext; - entity?: EntityAdapter; + entity?: EntityAdapter; // This is the target type in the cypher response, if it is a node cypherAttributeField: AttributeAdapter; cypherArguments?: Record; + isEdge: boolean; }): CypherEntityOperation | CompositeCypherOperation | CypherAttributeOperation { const selection = new CustomCypherSelection({ operationField: cypherAttributeField, rawArguments: cypherArguments, isNested: true, + attachedTo: isEdge ? "relationship" : "node", }); if (!entity) { return new CypherAttributeOperation(selection, cypherAttributeField, true); diff --git a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts index 186687c724..88af7e3310 100644 --- a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts @@ -34,7 +34,6 @@ import { CypherPropertySort } from "../ast/sort/CypherPropertySort"; import { PropertySort } from "../ast/sort/PropertySort"; import { ScoreSort } from "../ast/sort/ScoreSort"; import type { Sort } from "../ast/sort/Sort"; -import { isConcreteEntity } from "../utils/is-concrete-entity"; import { isRelationshipEntity } from "../utils/is-relationship-entity"; import { isUnionEntity } from "../utils/is-union-entity"; import type { QueryASTFactory } from "./QueryASTFactory"; @@ -149,10 +148,12 @@ export class SortAndPaginationFactory { if (!attribute) { throw new Error(`no filter attribute ${fieldName}`); } - if (attribute.annotations.cypher && isConcreteEntity(entity)) { + + if (attribute.annotations.cypher) { const cypherOperation = this.queryASTFactory.operationsFactory.createCustomCypherOperation({ context, cypherAttributeField: attribute, + isEdge: entity instanceof RelationshipAdapter, }); if (!(cypherOperation instanceof CypherAttributeOperation)) { throw new Error("Transpile error: sorting is supported only for @cypher scalar properties"); diff --git a/packages/graphql/tests/integration/directives/cypher/cyher-sort.int.test.ts b/packages/graphql/tests/integration/directives/cypher/cyher-sort.int.test.ts new file mode 100644 index 0000000000..02d3d644ef --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/cyher-sort.int.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("cypher directive sort", () => { + const testHelper = new TestHelper(); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} @node { + name: String! + actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + ranking: Int! @cypher(statement: """ + RETURN this.rank as ranking + """, columnName: "ranking") + } + + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("order nested relationship by relationship properties DESC", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(sort: {node: {ranking: DESC}}) { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN]-(:${Actor} {name: "Main actor", rank: 1}) + CREATE(m)<-[:ACTED_IN]-(:${Actor} {name: "Second actor", rank: 2})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Second actor", + }, + }, + { + node: { + name: "Main actor", + }, + }, + ], + }, + }, + ], + }); + }); + test("order nested relationship by relationship properties ASC", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(sort: {node: {ranking: ASC}}) { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN]-(:${Actor} {name: "Main actor", rank: 1}) + CREATE(m)<-[:ACTED_IN]-(:${Actor} {name: "Second actor", rank: 2})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Main actor", + }, + }, + { + node: { + name: "Second actor", + }, + }, + ], + }, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/cypher-params.int.test.ts b/packages/graphql/tests/integration/directives/cypher/cypher-params.int.test.ts similarity index 97% rename from packages/graphql/tests/integration/cypher-params.int.test.ts rename to packages/graphql/tests/integration/directives/cypher/cypher-params.int.test.ts index 639f86ffdc..3b06d68c22 100644 --- a/packages/graphql/tests/integration/cypher-params.int.test.ts +++ b/packages/graphql/tests/integration/directives/cypher/cypher-params.int.test.ts @@ -18,8 +18,8 @@ */ import { generate } from "randomstring"; -import type { UniqueType } from "../utils/graphql-types"; -import { TestHelper } from "../utils/tests-helper"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; describe("cypherParams", () => { const testHelper = new TestHelper(); diff --git a/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-interface.int.test.ts b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-interface.int.test.ts new file mode 100644 index 0000000000..efa9e7c0a9 --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-interface.int.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("cypher directive in relationship properties with interfaces", () => { + const testHelper = new TestHelper(); + + let Movie: UniqueType; + let Actor: UniqueType; + let Director: UniqueType; + let Series: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + Director = testHelper.createUniqueType("Director"); + Series = testHelper.createUniqueType("Series"); + + const typeDefs = /* GraphQL */ ` + interface Production { + title: String! + actors: [Person!]! @declareRelationship + } + + type ${Movie} implements Production @node { + title: String! + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Series} implements Production @node { + title: String! + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + interface Person { + name: String! + } + + type ${Actor} implements Person @node { + name: String! + actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ${Director} implements Person @node { + name: String! + } + + type ActedIn @relationshipProperties { + screenTimeHours: Float + @cypher( + statement: """ + RETURN this.screenTimeMinutes / 60 AS c + """ + columnName: "c" + ) + screenTimeMinutes: Int + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("custom properties on a interface relationship", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection { + edges { + properties { + ... on ActedIn { + screenTimeHours + } + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Keanu"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + properties: { + screenTimeHours: 2.0, + }, + }, + ], + }, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-props.int.test.ts b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-props.int.test.ts new file mode 100644 index 0000000000..7e0848f665 --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-props.int.test.ts @@ -0,0 +1,251 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("cypher directive in relationship properties", () => { + const testHelper = new TestHelper(); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Actor} @node { + name: String! + actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + screenTimeHours: Float + @cypher( + statement: """ + RETURN this.screenTimeMinutes / 60 AS c + """ + columnName: "c" + ) + screenTimeMinutes: Int + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should query custom query and return relationship properties", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection { + edges { + properties { + screenTimeHours + } + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Keanu"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + properties: { + screenTimeHours: 2.0, + }, + node: { + name: "Keanu", + }, + }, + ], + }, + }, + ], + }); + }); + + test("filter by relationship @cypher property without projection", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(where: {edge: {screenTimeHours: {gt: 1.0}}}) { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Main actor"}) + CREATE(m)<-[:ACTED_IN {screenTimeMinutes: 80}]-(:${Actor} {name: "Second actor"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Main actor", + }, + }, + ], + }, + }, + ], + }); + }); + + test("filter by relationship @cypher property with projection", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(where: {edge: {screenTimeHours: {gt: 1.0}}}) { + edges { + node { + name + } + properties { + screenTimeHours + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Main actor"}) + CREATE(m)<-[:ACTED_IN {screenTimeMinutes: 80}]-(:${Actor} {name: "Second actor"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Main actor", + }, + properties: { + screenTimeHours: 2.0, + }, + }, + ], + }, + }, + ], + }); + }); + + test("filter nested relationship by @cypher property with projection", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural}(where: {actorsConnection: {some: {edge: {screenTimeHours: {gt: 1.5}}}}}) { + title + actorsConnection { + edges { + node { + name + } + properties { + screenTimeHours + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Main actor"}) + CREATE(:${Movie} {title: "The Matrix Reloaded"})<-[:ACTED_IN {screenTimeMinutes: 80}]-(:${Actor} {name: "Second actor"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Main actor", + }, + properties: { + screenTimeHours: 2.0, + }, + }, + ], + }, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-sort.int.test.ts b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-sort.int.test.ts new file mode 100644 index 0000000000..9107da7e56 --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-sort.int.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("cypher directive in relationship properties", () => { + const testHelper = new TestHelper(); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Actor} @node { + name: String! + actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + screenTimeHours: Float + @cypher( + statement: """ + RETURN this.screenTimeMinutes / 60 AS c + """ + columnName: "c" + ) + screenTimeMinutes: Int + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("order nested relationship by relationship properties DESC", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(sort: {edge: {screenTimeHours: DESC}}) { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Main actor"}) + CREATE(m)<-[:ACTED_IN {screenTimeMinutes: 80}]-(:${Actor} {name: "Second actor"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Main actor", + }, + }, + { + node: { + name: "Second actor", + }, + }, + ], + }, + }, + ], + }); + }); + + test("order nested relationship by relationship properties ASC", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection(sort: {edge: {screenTimeHours: ASC}}) { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(m:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Main actor"}) + CREATE(m)<-[:ACTED_IN {screenTimeMinutes: 80}]-(:${Actor} {name: "Second actor"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + node: { + name: "Second actor", + }, + }, + { + node: { + name: "Main actor", + }, + }, + ], + }, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-union.int.test.ts b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-union.int.test.ts new file mode 100644 index 0000000000..a23254b778 --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/relationship-properties/cypher-in-relationship-union.int.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("cypher directive in relationship properties with unions", () => { + const testHelper = new TestHelper(); + + let Movie: UniqueType; + let Actor: UniqueType; + let Director: UniqueType; + let Series: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + Director = testHelper.createUniqueType("Director"); + Series = testHelper.createUniqueType("Series"); + + const typeDefs = /* GraphQL */ ` + union Production = ${Movie} | ${Series} + + type ${Movie} @node { + title: String! + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Series} @node { + title: String! + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + union Person = ${Actor} | ${Director} + + type ${Actor} @node { + name: String! + actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ${Director} @node { + name: String! + } + + type ActedIn @relationshipProperties { + screenTimeHours: Float + @cypher( + statement: """ + RETURN this.screenTimeMinutes / 60 AS c + """ + columnName: "c" + ) + screenTimeMinutes: Int + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("custom properties on a union relationship", async () => { + const source = /* GraphQL */ ` + query { + ${Movie.plural} { + title + actorsConnection { + edges { + properties { + ... on ActedIn { + screenTimeHours + } + } + } + } + } + } + `; + + await testHelper.executeCypher( + `CREATE(:${Movie} {title: "The Matrix"})<-[:ACTED_IN {screenTimeMinutes: 120}]-(:${Actor} {name: "Keanu"})` + ); + + const gqlResult = await testHelper.executeGraphQL(source); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [Movie.plural]: [ + { + title: "The Matrix", + actorsConnection: { + edges: [ + { + properties: { + screenTimeHours: 2.0, + }, + }, + ], + }, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/tck/issues/2670.test.ts b/packages/graphql/tests/tck/issues/2670.test.ts index de3210fc3f..1c1616fff2 100644 --- a/packages/graphql/tests/tck/issues/2670.test.ts +++ b/packages/graphql/tests/tck/issues/2670.test.ts @@ -67,7 +67,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } @@ -106,7 +106,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) < $param0 AS var4 } @@ -145,7 +145,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) > $param0 AS var4 } @@ -190,7 +190,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN min(size(this3.title)) = $param0 AS var4 } @@ -235,7 +235,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN avg(size(this3.title)) = $param0 AS var4 } @@ -277,7 +277,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN max(this2.intValue) < $param0 AS var4 } @@ -322,7 +322,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN min(this2.intValue) = $param0 AS var4 } @@ -361,7 +361,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } @@ -400,7 +400,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } @@ -492,7 +492,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } @@ -542,11 +542,11 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this5:IN_GENRE]-(this6:Series) RETURN min(size(this6.name)) = $param1 AS var7 } @@ -600,11 +600,11 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this5:IN_GENRE]-(this6:Series) RETURN min(size(this6.name)) = $param1 AS var7 } @@ -658,11 +658,11 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this5:IN_GENRE]-(this6:Series) RETURN min(size(this6.name)) = $param1 AS var7 } @@ -710,7 +710,7 @@ describe("https://github.com/neo4j/graphql/issues/2670", () => { MATCH (this:Movie) CALL (this) { MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:IN_GENRE]-(this3:Movie) RETURN count(this3) = $param0 AS var4 } diff --git a/packages/graphql/tests/tck/issues/2803.test.ts b/packages/graphql/tests/tck/issues/2803.test.ts index 309b207ba1..842d9f17e2 100644 --- a/packages/graphql/tests/tck/issues/2803.test.ts +++ b/packages/graphql/tests/tck/issues/2803.test.ts @@ -371,7 +371,7 @@ describe("https://github.com/neo4j/graphql/issues/2803", () => { MATCH (this:Actor) CALL (this) { MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:ACTED_IN]-(this3:Actor) CALL (this3) { MATCH (this3)-[this4:ACTED_IN]->(this5:Movie) @@ -381,7 +381,7 @@ describe("https://github.com/neo4j/graphql/issues/2803", () => { WHERE var6 = true RETURN count(this3) > 0 AS var7 } - CALL (this1) { + CALL (this1, this0) { MATCH (this1)<-[this2:ACTED_IN]-(this3:Actor) CALL (this3) { MATCH (this3)-[this8:ACTED_IN]->(this9:Movie) diff --git a/packages/graphql/tests/tck/issues/6005.test.ts b/packages/graphql/tests/tck/issues/6005.test.ts index c0e2ab81b2..e163c2c021 100644 --- a/packages/graphql/tests/tck/issues/6005.test.ts +++ b/packages/graphql/tests/tck/issues/6005.test.ts @@ -196,7 +196,7 @@ describe("https://github.com/neo4j/graphql/issues/6005", () => { WITH edge.node AS this0 CALL (this0) { MATCH (this0)-[this1:ACTED_IN]->(this2:Movie) - CALL (this2) { + CALL (this2, this1) { MATCH (this2)<-[this3:ACTED_IN]-(this4:Actor) WITH DISTINCT this4 RETURN count(this4) = $param0 AS var5