From cb83cf5242b93992a307de9608eab663b7c18b2e Mon Sep 17 00:00:00 2001 From: Alle <111279668+a-alle@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:31:05 +0100 Subject: [PATCH] Fix aliased fields case on interface relationship connection filters (#4916) --- .changeset/sixty-grapes-rest.md | 5 + .../property-filters/ParamPropertyFilter.ts | 2 + .../property-filters/PropertyFilter.ts | 48 ++- .../queryAST/factory/AuthFilterFactory.ts | 6 + .../queryAST/factory/FilterFactory.ts | 7 + ...r-interface-relationship-alias.int.test.ts | 329 ++++++++++++++++++ .../filtering/interface-relationships.test.ts | 213 ++++++++++++ 7 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 .changeset/sixty-grapes-rest.md create mode 100644 packages/graphql/tests/integration/filtering/filter-interface-relationship-alias.int.test.ts create mode 100644 packages/graphql/tests/tck/connections/filtering/interface-relationships.test.ts diff --git a/.changeset/sixty-grapes-rest.md b/.changeset/sixty-grapes-rest.md new file mode 100644 index 0000000000..75126aaefb --- /dev/null +++ b/.changeset/sixty-grapes-rest.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Fix aliased fields case on interface relationship connection filters diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/ParamPropertyFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/ParamPropertyFilter.ts index 58955a47f4..d641b4df22 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/ParamPropertyFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/ParamPropertyFilter.ts @@ -22,6 +22,7 @@ import type { QueryASTContext } from "../../QueryASTContext"; import { PropertyFilter } from "./PropertyFilter"; import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { FilterOperator } from "../Filter"; +import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; type CypherVariable = Cypher.Variable | Cypher.Property | Cypher.Param; @@ -35,6 +36,7 @@ export class ParamPropertyFilter extends PropertyFilter { operator: FilterOperator; isNot: boolean; attachedTo?: "node" | "relationship"; + relationship?: RelationshipAdapter; }) { super(options); this.comparisonValue = options.comparisonValue; diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts index f3d9db3a8b..fbc82bf0de 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts @@ -25,9 +25,12 @@ import type { QueryASTNode } from "../../QueryASTNode"; import type { FilterOperator } from "../Filter"; import { Filter } from "../Filter"; import { hasTarget } from "../../../utils/context-has-target"; +import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; export class PropertyFilter extends Filter { protected attribute: AttributeAdapter; + protected relationship: RelationshipAdapter | undefined; protected comparisonValue: unknown; protected operator: FilterOperator; protected isNot: boolean; // _NOT is deprecated @@ -35,12 +38,14 @@ export class PropertyFilter extends Filter { constructor({ attribute, + relationship, comparisonValue, operator, isNot, attachedTo, }: { attribute: AttributeAdapter; + relationship?: RelationshipAdapter; comparisonValue: unknown; operator: FilterOperator; isNot: boolean; @@ -48,6 +53,7 @@ export class PropertyFilter extends Filter { }) { super(); this.attribute = attribute; + this.relationship = relationship; this.comparisonValue = comparisonValue; this.operator = operator; this.isNot = isNot; @@ -63,7 +69,7 @@ export class PropertyFilter extends Filter { } public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate { - const prop = this.getPropertyRef(queryASTContext); + const prop = this.getPropertyRefOrAliasesCase(queryASTContext); if (this.comparisonValue === null) { return this.getNullPredicate(prop); @@ -74,6 +80,42 @@ export class PropertyFilter extends Filter { return this.wrapInNotIfNeeded(baseOperation); } + private getPropertyRefOrAliasesCase(queryASTContext: QueryASTContext): Cypher.Property | Cypher.Case { + const implementationsWithAlias = this.getAliasesToResolve(); + if (implementationsWithAlias) { + return this.generateCaseForAliasedFields(queryASTContext, implementationsWithAlias); + } + return this.getPropertyRef(queryASTContext); + } + + private getAliasesToResolve(): [string[], string][] | undefined { + if (!this.relationship || !(this.relationship.target instanceof InterfaceEntityAdapter)) { + return; + } + const aliasedImplementationsMap = this.relationship.target.getImplementationToAliasMapWhereAliased( + this.attribute + ); + if (!aliasedImplementationsMap.length) { + return; + } + return aliasedImplementationsMap; + } + + private generateCaseForAliasedFields( + queryASTContext: QueryASTContext, + concreteLabelsToAttributeAlias: [string[], string][] + ): Cypher.Case { + if (!hasTarget(queryASTContext)) throw new Error("No parent node found!"); + const aliasesCase = new Cypher.Case(); + for (const [labels, databaseName] of concreteLabelsToAttributeAlias) { + aliasesCase + .when(queryASTContext.target.hasLabels(...labels)) + .then(queryASTContext.target.property(databaseName)); + } + aliasesCase.else(queryASTContext.target.property(this.attribute.databaseName)); + return aliasesCase; + } + private getPropertyRef(queryASTContext: QueryASTContext): Cypher.Property { if (this.attachedTo === "node") { if (!hasTarget(queryASTContext)) throw new Error("No parent node found!"); @@ -88,7 +130,7 @@ export class PropertyFilter extends Filter { /** Returns the operation for a given filter. * To be overridden by subclasses */ - protected getOperation(prop: Cypher.Property): Cypher.ComparisonOp { + protected getOperation(prop: Cypher.Property | Cypher.Case): Cypher.ComparisonOp { return this.createBaseOperation({ operator: this.operator, property: prop, @@ -120,7 +162,7 @@ export class PropertyFilter extends Filter { return expr; } - private getNullPredicate(propertyRef: Cypher.Property): Cypher.Predicate { + private getNullPredicate(propertyRef: Cypher.Property | Cypher.Case): Cypher.Predicate { if (this.isNot) { return Cypher.isNotNull(propertyRef); } else { diff --git a/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts index ac4eb1eee9..d303011c10 100644 --- a/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts @@ -131,12 +131,14 @@ export class AuthFilterFactory extends FilterFactory { operator, isNot, attachedTo, + relationship, }: { attribute: AttributeAdapter; comparisonValue: unknown; operator: WhereOperator | undefined; isNot: boolean; attachedTo?: "node" | "relationship"; + relationship?: RelationshipAdapter; }): PropertyFilter { const filterOperator = operator || "EQ"; @@ -144,6 +146,7 @@ export class AuthFilterFactory extends FilterFactory { if (typeof comparisonValue === "boolean") { return new ParamPropertyFilter({ attribute, + relationship, comparisonValue: new Cypher.Param(comparisonValue), isNot, operator: filterOperator, @@ -159,6 +162,7 @@ export class AuthFilterFactory extends FilterFactory { if (isCypherVariable) { return new ParamPropertyFilter({ attribute, + relationship, comparisonValue: comparisonValue, isNot, operator: filterOperator, @@ -168,6 +172,7 @@ export class AuthFilterFactory extends FilterFactory { if (comparisonValue === null) { return new PropertyFilter({ attribute, + relationship, comparisonValue: comparisonValue, isNot, operator: filterOperator, @@ -176,6 +181,7 @@ export class AuthFilterFactory extends FilterFactory { } return new ParamPropertyFilter({ attribute, + relationship, comparisonValue: new Cypher.Param(comparisonValue), isNot, operator: filterOperator, diff --git a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts index e303754ed6..4a125a756d 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -153,6 +153,7 @@ export class FilterFactory { return this.createInterfaceNodeFilters({ entity, whereFields: value, + relationship: rel, }); } return this.createNodeFilters(entity, value); @@ -164,12 +165,14 @@ export class FilterFactory { protected createPropertyFilter({ attribute, + relationship, comparisonValue, operator, isNot, attachedTo, }: { attribute: AttributeAdapter; + relationship?: RelationshipAdapter; comparisonValue: unknown; operator: WhereOperator | undefined; isNot: boolean; @@ -197,6 +200,7 @@ export class FilterFactory { return new PropertyFilter({ attribute, + relationship, comparisonValue, isNot, operator: filterOperator, @@ -267,10 +271,12 @@ export class FilterFactory { entity, targetEntity, whereFields, + relationship, }: { entity: InterfaceEntityAdapter; targetEntity?: ConcreteEntityAdapter; whereFields: Record; + relationship?: RelationshipAdapter; }): Filter[] { const filters = filterTruthy( Object.entries(whereFields).flatMap(([key, value]): Filter | Filter[] | undefined => { @@ -317,6 +323,7 @@ export class FilterFactory { } return this.createPropertyFilter({ attribute: attr, + relationship, comparisonValue: value, isNot, operator, diff --git a/packages/graphql/tests/integration/filtering/filter-interface-relationship-alias.int.test.ts b/packages/graphql/tests/integration/filtering/filter-interface-relationship-alias.int.test.ts new file mode 100644 index 0000000000..0986ee48f5 --- /dev/null +++ b/packages/graphql/tests/integration/filtering/filter-interface-relationship-alias.int.test.ts @@ -0,0 +1,329 @@ +/* + * 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 { generate } from "randomstring"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import type { UniqueType } from "../../utils/graphql-types"; +import { TestHelper } from "../../utils/tests-helper"; + +describe("interface relationships aliased fields", () => { + const testHelper = new TestHelper(); + const secret = "secret"; + + let typeMovie: UniqueType; + let typeSeries: UniqueType; + let typeActor: UniqueType; + let ProtectedActor: UniqueType; + + beforeEach(async () => { + typeMovie = testHelper.createUniqueType("Movie"); + typeSeries = testHelper.createUniqueType("Series"); + typeActor = testHelper.createUniqueType("Actor"); + ProtectedActor = testHelper.createUniqueType("ProtectedActor"); + + const typeDefs = /* GraphQL */ ` + interface Production { + title: String! + } + + type ${typeMovie} implements Production { + title: String! @alias(property: "movieTitle") + runtime: Int! + } + + type ${typeSeries} implements Production { + title: String! @alias(property: "seriesTitle") + episodes: Int! + } + + type ActedIn @relationshipProperties { + screenTime: Int! + } + + type ${typeActor} { + name: String! + currentlyActingIn: Production @relationship(type: "CURRENTLY_ACTING_IN", direction: OUT) + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ${ProtectedActor} @authorization(validate: [{ where: { node: { actedInConnection: { node: { title: "$jwt.title" } } } } }]) { + name: String! @alias(property: "dbName") + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs, features: { authorization: { key: secret } } }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should read and return interface relationship fields with interface relationship filter SOME", async () => { + const actorName = generate({ + readable: true, + charset: "alphabetic", + }); + const actorName2 = generate({ + readable: true, + charset: "alphabetic", + }); + + const movieTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const movieTitle2 = generate({ + readable: true, + charset: "alphabetic", + }); + const movieRuntime = 123; + const movieScreenTime = 23; + + const seriesTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const seriesEpisodes = 234; + const seriesScreenTime = 45; + + const query = /* GraphQL */ ` + query Actors($title: String) { + ${typeActor.plural}(where: { actedInConnection_SOME: { node: { title: $title } } }) { + name + actedIn { + title + ... on ${typeMovie} { + runtime + } + ... on ${typeSeries} { + episodes + } + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (a:${typeActor} { name: $actorName }) + CREATE (a)-[:ACTED_IN { screenTime: $movieScreenTime }]->(:${typeMovie} { movieTitle: $movieTitle, runtime:$movieRuntime }) + CREATE (a)-[:ACTED_IN { screenTime: $seriesScreenTime }]->(:${typeSeries} { seriesTitle: $seriesTitle, episodes: $seriesEpisodes }) + CREATE (a2:${typeActor} { name: $actorName2 }) + CREATE (a2)-[:ACTED_IN { screenTime: $movieScreenTime }]->(:${typeMovie} { movieTitle: $movieTitle2, runtime:$movieRuntime }) + `, + { + actorName, + actorName2, + movieTitle, + movieTitle2, + movieRuntime, + movieScreenTime, + seriesTitle, + seriesEpisodes, + seriesScreenTime, + } + ); + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { title: movieTitle2 }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [typeActor.plural]: [ + { + actedIn: expect.toIncludeSameMembers([ + { + runtime: movieRuntime, + title: movieTitle2, + }, + ]), + name: actorName2, + }, + ], + }); + }); + + test("delete", async () => { + const actorName = generate({ + readable: true, + charset: "alphabetic", + }); + const actorName2 = generate({ + readable: true, + charset: "alphabetic", + }); + + const movieTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const movieTitle2 = generate({ + readable: true, + charset: "alphabetic", + }); + const movieRuntime = 123; + const movieScreenTime = 23; + + const seriesTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const seriesEpisodes = 234; + const seriesScreenTime = 45; + + const query = /* GraphQL */ ` + mutation deleteActors($title: String) { + ${typeActor.operations.delete}(where: { actedInConnection_SOME: { node: { title: $title } } }) { + nodesDeleted + relationshipsDeleted + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (a:${typeActor} { name: $actorName }) + CREATE (a)-[:ACTED_IN { screenTime: $movieScreenTime }]->(:${typeMovie} { movieTitle: $movieTitle, runtime:$movieRuntime }) + CREATE (a)-[:ACTED_IN { screenTime: $seriesScreenTime }]->(:${typeSeries} { seriesTitle: $seriesTitle, episodes: $seriesEpisodes }) + CREATE (a2:${typeActor} { name: $actorName2 }) + CREATE (a2)-[:ACTED_IN { screenTime: $movieScreenTime }]->(:${typeMovie} { movieTitle: $movieTitle2, runtime:$movieRuntime }) + `, + { + actorName, + actorName2, + movieTitle, + movieTitle2, + movieRuntime, + movieScreenTime, + seriesTitle, + seriesEpisodes, + seriesScreenTime, + } + ); + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { title: movieTitle2 }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data?.[typeActor.operations.delete]).toEqual({ + nodesDeleted: 1, + relationshipsDeleted: 1, + }); + }); + + test("auth", async () => { + const actorName = generate({ + readable: true, + charset: "alphabetic", + }); + const actorName2 = generate({ + readable: true, + charset: "alphabetic", + }); + const protectedActorName = generate({ + readable: true, + charset: "alphabetic", + }); + + const movieTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const movieTitle2 = generate({ + readable: true, + charset: "alphabetic", + }); + const movieRuntime = 123; + const movieScreenTime = 23; + + const seriesTitle = generate({ + readable: true, + charset: "alphabetic", + }); + const seriesEpisodes = 234; + const seriesScreenTime = 45; + const query = /* GraphQL */ ` + query ProtectedActors { + ${ProtectedActor.plural} { + name + actedIn { + title + ... on ${typeMovie} { + runtime + } + ... on ${typeSeries} { + episodes + } + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (a:${typeActor} { name: $actorName }) + CREATE (a)-[:ACTED_IN { screenTime: $movieScreenTime }]->(:${typeMovie} { movieTitle: $movieTitle, runtime:$movieRuntime }) + CREATE (a)-[:ACTED_IN { screenTime: $seriesScreenTime }]->(:${typeSeries} { seriesTitle: $seriesTitle, episodes: $seriesEpisodes }) + CREATE (a2:${typeActor} { name: $actorName2 }) + CREATE (m:${typeMovie} { movieTitle: $movieTitle2, runtime:$movieRuntime }) + CREATE (a2)-[:ACTED_IN { screenTime: $movieScreenTime }]->(m) + CREATE (pa:${ProtectedActor} { dbName: $protectedActorName }) + CREATE (pa)-[:ACTED_IN { screenTime: $movieScreenTime }]->(m) + `, + { + actorName, + actorName2, + protectedActorName, + movieTitle, + movieTitle2, + movieRuntime, + movieScreenTime, + seriesTitle, + seriesEpisodes, + seriesScreenTime, + } + ); + + const tokenTitle = movieTitle2; + const token = createBearerToken(secret, { roles: ["reader"], title: tokenTitle }); + + const gqlResult = await testHelper.executeGraphQLWithToken(query, token); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult.data).toEqual({ + [ProtectedActor.plural]: [ + { + actedIn: expect.toIncludeSameMembers([ + { + runtime: movieRuntime, + title: movieTitle2, + }, + ]), + name: protectedActorName, + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/tck/connections/filtering/interface-relationships.test.ts b/packages/graphql/tests/tck/connections/filtering/interface-relationships.test.ts new file mode 100644 index 0000000000..38884fb726 --- /dev/null +++ b/packages/graphql/tests/tck/connections/filtering/interface-relationships.test.ts @@ -0,0 +1,213 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; + +describe("interface relationships with aliased fields", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + interface Production { + title: String! + } + + type Movie implements Production { + title: String! @alias(property: "movieTitle") + runtime: Int! + } + + type Series implements Production { + title: String! @alias(property: "seriesTitle") + episodes: Int! + } + + type ActedIn @relationshipProperties { + screenTime: Int! + } + + type Actor { + name: String! + currentlyActingIn: Production @relationship(type: "CURRENTLY_ACTING_IN", direction: OUT) + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ProtectedActor + @authorization( + validate: [{ where: { node: { actedInConnection: { node: { title: "$jwt.title" } } } } }] + ) { + name: String! @alias(property: "dbName") + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should read and return interface relationship fields with interface relationship filter SOME", async () => { + const query = /* GraphQL */ ` + query Actors($title: String) { + actors(where: { actedInConnection_SOME: { node: { title: $title } } }) { + name + actedIn { + title + ... on Movie { + runtime + } + ... on Series { + episodes + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { + variableValues: { title: "movieTitle2" }, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WHERE EXISTS { + MATCH (this)-[this0:ACTED_IN]->(this1) + WHERE (CASE + WHEN this1:Movie THEN this1.movieTitle + WHEN this1:Series THEN this1.seriesTitle + ELSE this1.title + END = $param0 AND (this1:Movie OR this1:Series)) + } + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .runtime, title: this3.movieTitle, __resolveType: \\"Movie\\", __id: id(this3) } AS this3 + RETURN this3 AS var4 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .episodes, title: this6.seriesTitle, __resolveType: \\"Series\\", __id: id(this6) } AS this6 + RETURN this6 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 + } + RETURN this { .name, actedIn: var4 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"movieTitle2\\" + }" + `); + }); + + test("delete", async () => { + const query = /* GraphQL */ ` + mutation deleteActors($title: String) { + deleteActors(where: { actedInConnection_SOME: { node: { title: $title } } }) { + nodesDeleted + relationshipsDeleted + } + } + `; + + const result = await translateQuery(neoSchema, query, { + variableValues: { title: "movieTitle2" }, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WHERE EXISTS { + MATCH (this)-[this0:ACTED_IN]->(this1) + WHERE (CASE + WHEN this1:Movie THEN this1.movieTitle + WHEN this1:Series THEN this1.seriesTitle + ELSE this1.title + END = $param0 AND (this1:Movie OR this1:Series)) + } + DETACH DELETE this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"movieTitle2\\" + }" + `); + }); + + test("auth", async () => { + const query = /* GraphQL */ ` + query ProtectedActors { + protectedActors { + name + actedIn { + title + ... on Movie { + runtime + } + ... on Series { + episodes + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:ProtectedActor) + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)-[this1:ACTED_IN]->(this0) WHERE (($jwt.title IS NOT NULL AND CASE + WHEN this0:Movie THEN this0.movieTitle + WHEN this0:Series THEN this0.seriesTitle + ELSE this0.title + END = $jwt.title) AND (this0:Movie OR this0:Series)) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .runtime, title: this3.movieTitle, __resolveType: \\"Movie\\", __id: id(this3) } AS this3 + RETURN this3 AS var4 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .episodes, title: this6.seriesTitle, __resolveType: \\"Series\\", __id: id(this6) } AS this6 + RETURN this6 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 + } + RETURN this { name: this.dbName, actedIn: var4 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"jwt\\": {} + }" + `); + }); +});