diff --git a/docs/asciidoc/type-definitions/unions-and-interfaces.adoc b/docs/asciidoc/type-definitions/unions-and-interfaces.adoc index 2da85b70b4..1980e23cab 100644 --- a/docs/asciidoc/type-definitions/unions-and-interfaces.adoc +++ b/docs/asciidoc/type-definitions/unions-and-interfaces.adoc @@ -31,9 +31,9 @@ Below you can find some examples of how queries and mutations work with this exa === Querying a union -The Neo4j GraphQL Library only returns union members which have inline fragments in your selection set. +Which union members are returned by a Query are dictated by the `where` filter applied. -For example, the following will return users and only their blogs: +For example, the following will return all user content, and you will specifically get the title of each blog. [source, graphql] ---- @@ -43,37 +43,73 @@ query GetUsersWithBlogs { content { ... on Blog { title - posts { - content - } } } } } ---- -Whilst the query below will return both the blogs and posts of users: +Whilst the query below will only return blogs. We could for instance use a filter to check that the title is not null to essentially return all blogs: [source, graphql] ---- query GetUsersWithAllContent { users { name - content { + content(where: { Blog: { title_NOT: null }}) { ... on Blog { title - posts { - content - } - } - ... on Post { - content } } } } ---- +Conceptually, this maps to the `WHERE` clauses of the subquery unions in Cypher. Going back to the first example with no `where` argument, each subquery has a similar structure: + +[source, cypher] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + RETURN { __resolveType: "Blog", title: blog.title } +UNION + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) + RETURN { __resolveType: "Post" } +} +---- + +Now if we were to leave both subqueries and add a `WHERE` clause for blogs, it would look like this: + +[source, cypher] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + WHERE blog.title IS NOT NULL + RETURN { __resolveType: "Blog", title: blog.title } +UNION + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(journal:Post) + RETURN { __resolveType: "Post" } +} +---- + +As you can see, the subqueries are now "unbalanced", which could result in massive overfetching of `Post` nodes. + +So, when a `where` argument is passed in, we only include union members which are in the `where` object, so it is essentially acting as a logical OR gate, different from the rest of our `where` arguments: + +[source, cypher] +---- +CALL { + WITH this + OPTIONAL MATCH (this)-[has_content:HAS_CONTENT]->(blog:Blog) + WHERE blog.title IS NOT NULL + RETURN { __resolveType: "Blog", title: blog.title } +} +---- + === Creating a union The below mutation creates the user and their content: diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index ae2eaca259..e8db681f88 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -1137,20 +1137,9 @@ function makeAugmentedSchema( const totalCount = isInt(count) ? count.toNumber() : count; - const unionEdges = edges?.filter((edge) => { - if ( - Object.keys(edge.node).length === 1 && - Object.prototype.hasOwnProperty.call(edge.node, "__resolveType") - ) { - return false; - } - - return true; - }); - return { totalCount, - ...createConnectionWithEdgeProperties(unionEdges, args, totalCount), + ...createConnectionWithEdgeProperties(edges, args, totalCount), }; }, }, diff --git a/packages/graphql/src/translate/connection/create-connection-and-params.ts b/packages/graphql/src/translate/connection/create-connection-and-params.ts index 324acc9b33..76814966fe 100644 --- a/packages/graphql/src/translate/connection/create-connection-and-params.ts +++ b/packages/graphql/src/translate/connection/create-connection-and-params.ts @@ -89,143 +89,147 @@ function createConnectionAndParams({ const unionSubqueries: string[] = []; unionNodes.forEach((n) => { - const relatedNodeVariable = `${nodeVariable}_${n.name}`; - const nodeOutStr = `(${relatedNodeVariable}:${n.name})`; + if (!whereInput || Object.prototype.hasOwnProperty.call(whereInput, n.name)) { + const relatedNodeVariable = `${nodeVariable}_${n.name}`; + const nodeOutStr = `(${relatedNodeVariable}:${n.name})`; - const unionSubquery: string[] = []; - const unionSubqueryElementsToCollect = [...elementsToCollect]; + const unionSubquery: string[] = []; + const unionSubqueryElementsToCollect = [...elementsToCollect]; - const nestedSubqueries: string[] = []; + const nestedSubqueries: string[] = []; - if (node) { - // Doing this for unions isn't necessary, but this would also work for interfaces if we decided to take that direction - const nodeFieldsByTypeName: FieldsByTypeName = { - [n.name]: { - ...node?.fieldsByTypeName[n.name], - ...node?.fieldsByTypeName[field.relationship.typeMeta.name], - }, - }; + if (node) { + const nodeFieldsByTypeName: FieldsByTypeName = { + [n.name]: { + ...node?.fieldsByTypeName[n.name], + ...node?.fieldsByTypeName[field.relationship.typeMeta.name], + }, + }; - const nodeProjectionAndParams = createProjectionAndParams({ - fieldsByTypeName: nodeFieldsByTypeName, - node: n, - context, - varName: relatedNodeVariable, - literalElements: true, - resolveType: true, - }); - const [nodeProjection, nodeProjectionParams] = nodeProjectionAndParams; - unionSubqueryElementsToCollect.push(`node: ${nodeProjection}`); - globalParams = { - ...globalParams, - ...nodeProjectionParams, - }; - - if (nodeProjectionAndParams[2]?.connectionFields?.length) { - nodeProjectionAndParams[2].connectionFields.forEach((connectionResolveTree) => { - const connectionField = n.connectionFields.find( - (x) => x.fieldName === connectionResolveTree.name - ) as ConnectionField; - const nestedConnection = createConnectionAndParams({ - resolveTree: connectionResolveTree, - field: connectionField, - context, - nodeVariable: relatedNodeVariable, - parameterPrefix: `${parameterPrefix ? `${parameterPrefix}.` : `${nodeVariable}_`}${ - resolveTree.name - }.edges.node`, - }); - nestedSubqueries.push(nestedConnection[0]); - - globalParams = { - ...globalParams, - ...Object.entries(nestedConnection[1]).reduce>((res, [k, v]) => { - if (k !== `${relatedNodeVariable}_${connectionResolveTree.name}`) { - res[k] = v; - } - return res; - }, {}), - }; + const nodeProjectionAndParams = createProjectionAndParams({ + fieldsByTypeName: nodeFieldsByTypeName, + node: n, + context, + varName: relatedNodeVariable, + literalElements: true, + resolveType: true, + }); + const [nodeProjection, nodeProjectionParams] = nodeProjectionAndParams; + unionSubqueryElementsToCollect.push(`node: ${nodeProjection}`); + globalParams = { + ...globalParams, + ...nodeProjectionParams, + }; - if (nestedConnection[1][`${relatedNodeVariable}_${connectionResolveTree.name}`]) { - if (!nestedConnectionFieldParams) nestedConnectionFieldParams = {}; - nestedConnectionFieldParams = { - ...nestedConnectionFieldParams, - ...{ - [connectionResolveTree.name]: - nestedConnection[1][`${relatedNodeVariable}_${connectionResolveTree.name}`], - }, + if (nodeProjectionAndParams[2]?.connectionFields?.length) { + nodeProjectionAndParams[2].connectionFields.forEach((connectionResolveTree) => { + const connectionField = n.connectionFields.find( + (x) => x.fieldName === connectionResolveTree.name + ) as ConnectionField; + const nestedConnection = createConnectionAndParams({ + resolveTree: connectionResolveTree, + field: connectionField, + context, + nodeVariable: relatedNodeVariable, + parameterPrefix: `${parameterPrefix ? `${parameterPrefix}.` : `${nodeVariable}_`}${ + resolveTree.name + }.edges.node`, + }); + nestedSubqueries.push(nestedConnection[0]); + + globalParams = { + ...globalParams, + ...Object.entries(nestedConnection[1]).reduce>( + (res, [k, v]) => { + if (k !== `${relatedNodeVariable}_${connectionResolveTree.name}`) { + res[k] = v; + } + return res; + }, + {} + ), }; - } - }); - } - } else { - // This ensures that totalCount calculation is accurate if edges not asked for - unionSubqueryElementsToCollect.push(`node: { __resolveType: "${n.name}" }`); - } - unionSubquery.push(`WITH ${nodeVariable}`); - unionSubquery.push(`OPTIONAL MATCH (${nodeVariable})${inStr}${relTypeStr}${outStr}${nodeOutStr}`); + if (nestedConnection[1][`${relatedNodeVariable}_${connectionResolveTree.name}`]) { + if (!nestedConnectionFieldParams) nestedConnectionFieldParams = {}; + nestedConnectionFieldParams = { + ...nestedConnectionFieldParams, + ...{ + [connectionResolveTree.name]: + nestedConnection[1][`${relatedNodeVariable}_${connectionResolveTree.name}`], + }, + }; + } + }); + } + } else { + // This ensures that totalCount calculation is accurate if edges not asked for + unionSubqueryElementsToCollect.push(`node: { __resolveType: "${n.name}" }`); + } - const allowAndParams = createAuthAndParams({ - operation: "READ", - entity: n, - context, - allow: { - parentNode: n, - varName: relatedNodeVariable, - }, - }); - if (allowAndParams[0]) { - globalParams = { ...globalParams, ...allowAndParams[1] }; - unionSubquery.push( - `CALL apoc.util.validate(NOT(${allowAndParams[0]}), "${AUTH_FORBIDDEN_ERROR}", [0])` - ); - } + unionSubquery.push(`WITH ${nodeVariable}`); + unionSubquery.push(`OPTIONAL MATCH (${nodeVariable})${inStr}${relTypeStr}${outStr}${nodeOutStr}`); - const whereStrs: string[] = []; - const unionWhere = (whereInput || {})[n.name]; - if (unionWhere) { - const where = createConnectionWhereAndParams({ - whereInput: unionWhere, - node: n, - nodeVariable: relatedNodeVariable, - relationship, - relationshipVariable, + const allowAndParams = createAuthAndParams({ + operation: "READ", + entity: n, context, - parameterPrefix: `${parameterPrefix ? `${parameterPrefix}.` : `${nodeVariable}_`}${ - resolveTree.name - }.args.where.${n.name}`, + allow: { + parentNode: n, + varName: relatedNodeVariable, + }, }); - const [whereClause] = where; - if (whereClause) { - whereStrs.push(whereClause); + if (allowAndParams[0]) { + globalParams = { ...globalParams, ...allowAndParams[1] }; + unionSubquery.push( + `CALL apoc.util.validate(NOT(${allowAndParams[0]}), "${AUTH_FORBIDDEN_ERROR}", [0])` + ); } - } - const whereAuth = createAuthAndParams({ - operation: "READ", - entity: n, - context, - where: { varName: relatedNodeVariable, node: n }, - }); - if (whereAuth[0]) { - whereStrs.push(whereAuth[0]); - globalParams = { ...globalParams, ...whereAuth[1] }; - } + const whereStrs: string[] = []; + const unionWhere = (whereInput || {})[n.name]; + if (unionWhere) { + const where = createConnectionWhereAndParams({ + whereInput: unionWhere, + node: n, + nodeVariable: relatedNodeVariable, + relationship, + relationshipVariable, + context, + parameterPrefix: `${parameterPrefix ? `${parameterPrefix}.` : `${nodeVariable}_`}${ + resolveTree.name + }.args.where.${n.name}`, + }); + const [whereClause] = where; + if (whereClause) { + whereStrs.push(whereClause); + } + } - if (whereStrs.length) { - unionSubquery.push(`WHERE ${whereStrs.join(" AND ")}`); - } + const whereAuth = createAuthAndParams({ + operation: "READ", + entity: n, + context, + where: { varName: relatedNodeVariable, node: n }, + }); + if (whereAuth[0]) { + whereStrs.push(whereAuth[0]); + globalParams = { ...globalParams, ...whereAuth[1] }; + } - if (nestedSubqueries.length) { - unionSubquery.push(nestedSubqueries.join("\n")); - } + if (whereStrs.length) { + unionSubquery.push(`WHERE ${whereStrs.join(" AND ")}`); + } - unionSubquery.push(`WITH { ${unionSubqueryElementsToCollect.join(", ")} } AS edge`); - unionSubquery.push("RETURN edge"); + if (nestedSubqueries.length) { + unionSubquery.push(nestedSubqueries.join("\n")); + } - unionSubqueries.push(unionSubquery.join("\n")); + unionSubquery.push(`WITH { ${unionSubqueryElementsToCollect.join(", ")} } AS edge`); + unionSubquery.push("RETURN edge"); + + unionSubqueries.push(unionSubquery.join("\n")); + } }); const unionSubqueryCypher = ["CALL {", unionSubqueries.join("\nUNION\n"), "}"]; diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index 7bc3516148..4a08257b75 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -259,19 +259,12 @@ function createProjectionAndParams({ const isArray = relationField.typeMeta.array; if (relationField.union) { - let referenceNodes = context.neoSchema.nodes.filter( + const referenceNodes = context.neoSchema.nodes.filter( (x) => relationField.union?.nodes?.includes(x.name) && - Object.prototype.hasOwnProperty.call(fieldFields, x.name) + (!field.args.where || Object.prototype.hasOwnProperty.call(field.args.where, x.name)) ); - // If for example, just selecting __typename, error will be thrown without this - if (!referenceNodes.length) { - referenceNodes = context.neoSchema.nodes.filter((x) => - relationField.union?.nodes?.includes(x.name) - ); - } - const unionStrs: string[] = [ `${key}: ${!isArray ? "head(" : ""} [(${ chainStr || varName diff --git a/packages/graphql/tests/integration/connections/unions.int.test.ts b/packages/graphql/tests/integration/connections/unions.int.test.ts index 108874e2a1..735ce0b1be 100644 --- a/packages/graphql/tests/integration/connections/unions.int.test.ts +++ b/packages/graphql/tests/integration/connections/unions.int.test.ts @@ -141,6 +141,64 @@ describe("Connections -> Unions", () => { } }); + test("Projecting node and relationship properties for one union member with no arguments", async () => { + const session = driver.session(); + + const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + + const query = ` + query { + authors(where: { name: "Charles Dickens" }) { + name + publicationsConnection { + edges { + words + node { + ... on Book { + title + } + } + } + } + } + } + `; + + try { + await neoSchema.checkNeo4jCompat(); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeFalsy(); + + expect(result?.data?.authors).toEqual([ + { + name: "Charles Dickens", + publicationsConnection: { + edges: [ + { + words: 167543, + node: { + title: "Oliver Twist", + }, + }, + { + words: 3413, + node: {}, + }, + ], + }, + }, + ]); + } finally { + await session.close(); + } + }); + test("With where argument on node", async () => { const session = driver.session(); diff --git a/packages/graphql/tests/integration/nested-unions.int.test.ts b/packages/graphql/tests/integration/nested-unions.int.test.ts index 4c3e9e36a4..16320b95d9 100644 --- a/packages/graphql/tests/integration/nested-unions.int.test.ts +++ b/packages/graphql/tests/integration/nested-unions.int.test.ts @@ -117,21 +117,11 @@ describe("Nested unions", () => { contextValue: { driver }, }); expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.updateMovies.movies).toEqual([ - { - title: movieTitle, - actors: [ - { - name: actorName, - actedIn: [ - { - name: seriesName, - }, - ], - }, - ], - }, - ]); + expect(gqlResult.data?.updateMovies.movies[0].title).toEqual(movieTitle); + expect(gqlResult.data?.updateMovies.movies[0].actors[0].name).toEqual(actorName); + expect(gqlResult.data?.updateMovies.movies[0].actors[0].actedIn).toContainEqual({ + name: seriesName, + }); } finally { await session.close(); } @@ -377,21 +367,11 @@ describe("Nested unions", () => { contextValue: { driver }, }); expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.updateMovies.movies).toEqual([ - { - title: movieTitle, - actors: [ - { - name: actorName, - actedIn: [ - { - name: seriesName, - }, - ], - }, - ], - }, - ]); + expect(gqlResult.data?.updateMovies.movies[0].title).toEqual(movieTitle); + expect(gqlResult.data?.updateMovies.movies[0].actors[0].name).toEqual(actorName); + expect(gqlResult.data?.updateMovies.movies[0].actors[0].actedIn).toContainEqual({ + name: seriesName, + }); const cypherMovie = ` MATCH (m:Movie {title: $movieTitle}) diff --git a/packages/graphql/tests/integration/unions.int.test.ts b/packages/graphql/tests/integration/unions.int.test.ts index 425c2f6c0d..9227161a5d 100644 --- a/packages/graphql/tests/integration/unions.int.test.ts +++ b/packages/graphql/tests/integration/unions.int.test.ts @@ -105,6 +105,74 @@ describe("unions", () => { } }); + test("should read and return correct union members with where argument", async () => { + const session = driver.session(); + + const typeDefs = ` + union Search = Movie | Genre + + type Genre { + name: String + } + + type Movie { + title: String + search: [Search] @relationship(type: "SEARCH", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + resolvers: {}, + }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const genreName = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies (where: {title: "${movieTitle}"}) { + search(where: { Genre: { name: "${genreName}" }}) { + __typename + ... on Movie { + title + } + ... on Genre { + name + } + } + } + } + `; + + try { + await session.run(` + CREATE (m:Movie {title: "${movieTitle}"}) + CREATE (g:Genre {name: "${genreName}"}) + MERGE (m)-[:SEARCH]->(m) + MERGE (m)-[:SEARCH]->(g) + `); + const gqlResult = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult.data as any).movies[0]).toEqual({ + search: [{ __typename: "Genre", name: genreName }], + }); + } finally { + await session.close(); + } + }); + test("should create a nested union", async () => { const session = driver.session(); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/connections/unions.md b/packages/graphql/tests/tck/tck-test-files/cypher/connections/unions.md index 472792f7a3..6b3b62fa68 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/connections/unions.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/connections/unions.md @@ -349,56 +349,3 @@ RETURN this { .name, publicationsConnection } as this ``` --- - -## Projecting only one member of union node and relationship properties with no arguments - -### GraphQL Input - -```graphql -query { - authors { - name - publicationsConnection { - edges { - words - node { - ... on Book { - title - } - } - } - } - } -} -``` - -### Expected Cypher Output - -```cypher -MATCH (this:Author) -CALL { - WITH this - CALL { - WITH this - OPTIONAL MATCH (this)-[this_wrote:WROTE]->(this_Book:Book) - WITH { words: this_wrote.words, node: { __resolveType: "Book", title: this_Book.title } } AS edge - RETURN edge - UNION - WITH this - OPTIONAL MATCH (this)-[this_wrote:WROTE]->(this_Journal:Journal) - WITH { words: this_wrote.words, node: { __resolveType: "Journal" } } AS edge - RETURN edge - } - WITH collect(edge) as edges, count(edge) as totalCount - RETURN { edges: edges, totalCount: totalCount } AS publicationsConnection -} -RETURN this { .name, publicationsConnection } as this -``` - -### Expected Cypher Params - -```json -{} -``` - ---- diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/nested-unions.md b/packages/graphql/tests/tck/tck-test-files/cypher/nested-unions.md index 94cb0b0bc1..b60372f061 100644 --- a/packages/graphql/tests/tck/tck-test-files/cypher/nested-unions.md +++ b/packages/graphql/tests/tck/tck-test-files/cypher/nested-unions.md @@ -91,7 +91,7 @@ CALL { } RETURN count(*) } -RETURN this { .title, actors: [(this)<-[:ACTED_IN]-(this_actors) WHERE "LeadActor" IN labels(this_actors) | head( [ this_actors IN [this_actors] WHERE "LeadActor" IN labels (this_actors) | this_actors { __resolveType: "LeadActor", .name, actedIn: [(this_actors)-[:ACTED_IN]->(this_actors_actedIn) WHERE "Series" IN labels(this_actors_actedIn) | head( [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Series" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Series", .name } ] ) ] } ] ) ] } AS this +RETURN this { .title, actors: [(this)<-[:ACTED_IN]-(this_actors) WHERE "LeadActor" IN labels(this_actors) OR "Extra" IN labels(this_actors) | head( [ this_actors IN [this_actors] WHERE "LeadActor" IN labels (this_actors) | this_actors { __resolveType: "LeadActor", .name, actedIn: [(this_actors)-[:ACTED_IN]->(this_actors_actedIn) WHERE "Movie" IN labels(this_actors_actedIn) OR "Series" IN labels(this_actors_actedIn) | head( [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Movie" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Movie" } ] + [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Series" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Series", .name } ] ) ] } ] + [ this_actors IN [this_actors] WHERE "Extra" IN labels (this_actors) | this_actors { __resolveType: "Extra" } ] ) ] } AS this ``` ### Expected Cypher Params @@ -156,7 +156,7 @@ WITH this, this_disconnect_actors_LeadActor0 OPTIONAL MATCH (this_disconnect_actors_LeadActor0)-[this_disconnect_actors_LeadActor0_actedIn_Series0_rel:ACTED_IN]->(this_disconnect_actors_LeadActor0_actedIn_Series0:Series) WHERE this_disconnect_actors_LeadActor0_actedIn_Series0.name = $updateMovies.args.disconnect.actors.LeadActor[0].disconnect.actedIn.Series[0].where.node.name FOREACH(_ IN CASE this_disconnect_actors_LeadActor0_actedIn_Series0 WHEN NULL THEN [] ELSE [1] END | DELETE this_disconnect_actors_LeadActor0_actedIn_Series0_rel ) -RETURN this { .title, actors: [(this)<-[:ACTED_IN]-(this_actors) WHERE "LeadActor" IN labels(this_actors) | head( [ this_actors IN [this_actors] WHERE "LeadActor" IN labels (this_actors) | this_actors { __resolveType: "LeadActor", .name, actedIn: [(this_actors)-[:ACTED_IN]->(this_actors_actedIn) WHERE "Series" IN labels(this_actors_actedIn) | head( [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Series" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Series", .name } ] ) ] } ] ) ] } AS this +RETURN this { .title, actors: [(this)<-[:ACTED_IN]-(this_actors) WHERE "LeadActor" IN labels(this_actors) OR "Extra" IN labels(this_actors) | head( [ this_actors IN [this_actors] WHERE "LeadActor" IN labels (this_actors) | this_actors { __resolveType: "LeadActor", .name, actedIn: [(this_actors)-[:ACTED_IN]->(this_actors_actedIn) WHERE "Movie" IN labels(this_actors_actedIn) OR "Series" IN labels(this_actors_actedIn) | head( [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Movie" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Movie" } ] + [ this_actors_actedIn IN [this_actors_actedIn] WHERE "Series" IN labels (this_actors_actedIn) | this_actors_actedIn { __resolveType: "Series", .name } ] ) ] } ] + [ this_actors IN [this_actors] WHERE "Extra" IN labels (this_actors) | this_actors { __resolveType: "Extra" } ] ) ] } AS this ``` ### Expected Cypher Params