diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index c87c1bdd39..1e0118d193 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -27,6 +27,7 @@ import { EnumTypeDefinitionNode, GraphQLInt, GraphQLNonNull, + GraphQLResolveInfo, GraphQLSchema, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode, @@ -69,7 +70,7 @@ import getFieldTypeMeta from "./get-field-type-meta"; import Relationship, { RelationshipField } from "../classes/Relationship"; import getRelationshipFieldMeta from "./get-relationship-field-meta"; import getWhereFields from "./get-where-fields"; -import { createConnectionWithEdgeProperties } from "./pagination"; +import { connectionFieldResolver } from "./pagination"; import { validateDocument } from "./validation"; function makeAugmentedSchema( @@ -1149,15 +1150,13 @@ function makeAugmentedSchema( [connectionField.fieldName]: { type: connection.NonNull, args: composeNodeArgs, - resolve: (source, args: ConnectionQueryArgs) => { - const { totalCount: count, edges } = source[connectionField.fieldName]; - - const totalCount = isInt(count) ? count.toNumber() : count; - - return { - totalCount, - ...createConnectionWithEdgeProperties(edges, args, totalCount), - }; + resolve: (source, args: ConnectionQueryArgs, ctx, info: GraphQLResolveInfo) => { + return connectionFieldResolver({ + connectionField, + args, + info, + source, + }); }, }, }); diff --git a/packages/graphql/src/schema/pagination.test.ts b/packages/graphql/src/schema/pagination.test.ts index a4485a66c1..b70a5dc1c6 100644 --- a/packages/graphql/src/schema/pagination.test.ts +++ b/packages/graphql/src/schema/pagination.test.ts @@ -77,7 +77,12 @@ describe("cursor-pagination", () => { const totalCount = 50; expect(() => { - createConnectionWithEdgeProperties(arraySlice, args, totalCount); + createConnectionWithEdgeProperties({ + source: { edges: arraySlice }, + args, + totalCount, + selectionSet: undefined, + }); }).toThrow('Argument "first" must be a non-negative integer'); }); @@ -85,8 +90,12 @@ describe("cursor-pagination", () => { const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } })); const args = { after: "invalid" }; const totalCount = 50; - const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount); - + const result = createConnectionWithEdgeProperties({ + source: { edges: arraySlice }, + args, + totalCount, + selectionSet: undefined, + }); expect(result).toStrictEqual({ edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })), pageInfo: { @@ -102,7 +111,12 @@ describe("cursor-pagination", () => { const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } })); const args = {}; const totalCount = 50; - const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount); + const result = createConnectionWithEdgeProperties({ + source: { edges: arraySlice }, + args, + totalCount, + selectionSet: undefined, + }); expect(result).toStrictEqual({ edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index) })), pageInfo: { @@ -117,7 +131,12 @@ describe("cursor-pagination", () => { const arraySlice = [...Array(20).keys()].map((key) => ({ node: { id: key } })); const args = { after: offsetToCursor(10) }; const totalCount = 50; - const result = createConnectionWithEdgeProperties(arraySlice, args, totalCount); + const result = createConnectionWithEdgeProperties({ + source: { edges: arraySlice }, + args, + totalCount, + selectionSet: undefined, + }); expect(result).toStrictEqual({ edges: arraySlice.map((edge, index) => ({ ...edge, cursor: offsetToCursor(index + 11) })), pageInfo: { diff --git a/packages/graphql/src/schema/pagination.ts b/packages/graphql/src/schema/pagination.ts index 0590ca9b05..dcdc530126 100644 --- a/packages/graphql/src/schema/pagination.ts +++ b/packages/graphql/src/schema/pagination.ts @@ -17,17 +17,65 @@ * limitations under the License. */ +import { FieldNode, GraphQLResolveInfo, SelectionSetNode } from "graphql"; import { getOffsetWithDefault, offsetToCursor } from "graphql-relay/connection/arrayConnection"; import { Integer, isInt } from "neo4j-driver"; +import { ConnectionField, ConnectionQueryArgs } from "../types"; + +function getAliasKey({ selectionSet, key }: { selectionSet: SelectionSetNode | undefined; key: string }): string { + const selection = (selectionSet?.selections || []).find( + (x) => x.kind === "Field" && x.name.value === key + ) as FieldNode; + + if (selection?.alias) { + return selection.alias.value; + } + + return key; +} + +export function connectionFieldResolver({ + connectionField, + source, + args, + info, +}: { + connectionField: ConnectionField; + source: any; + args: ConnectionQueryArgs; + info: GraphQLResolveInfo; +}) { + const firstField = info.fieldNodes[0]; + const selectionSet = firstField.selectionSet; + + let value = source[connectionField.fieldName]; + if (firstField.alias) { + value = source[firstField.alias.value]; + } + + const totalCountKey = getAliasKey({ selectionSet, key: "totalCount" }); + const totalCount = value.totalCount; + + return { + [totalCountKey]: isInt(totalCount) ? totalCount.toNumber() : totalCount, + ...createConnectionWithEdgeProperties({ source: value, selectionSet, args, totalCount }), + }; +} /** * Adapted from graphql-relay-js ConnectionFromArraySlice */ -export function createConnectionWithEdgeProperties( - arraySlice: { node: Record; [key: string]: any }[] = [], - args: { after?: string; first?: number } = {}, - totalCount: number -) { +export function createConnectionWithEdgeProperties({ + selectionSet, + source, + args = {}, + totalCount, +}: { + selectionSet: SelectionSetNode | undefined; + source: any; + args: { after?: string; first?: number }; + totalCount: number; +}) { const { after, first } = args; if ((first as number) < 0) { @@ -40,24 +88,43 @@ export function createConnectionWithEdgeProperties( // increment the last cursor position by one for the sliceStart const sliceStart = lastEdgeCursor + 1; - const sliceEnd = sliceStart + ((first as number) || arraySlice.length); + const edges = source?.edges || []; - const edges = arraySlice.map((value, index) => { + const selections = selectionSet?.selections || []; + + const edgesField = selections.find((x) => x.kind === "Field" && x.name.value === "edges") as FieldNode; + const cursorKey = getAliasKey({ selectionSet: edgesField?.selectionSet, key: "cursor" }); + const nodeKey = getAliasKey({ selectionSet: edgesField?.selectionSet, key: "node" }); + + const sliceEnd = sliceStart + ((first as number) || edges.length); + + const mappedEdges = edges.map((value, index) => { return { ...value, - cursor: offsetToCursor(sliceStart + index), + ...(value.node ? { [nodeKey]: value.node } : {}), + [cursorKey]: offsetToCursor(sliceStart + index), }; }); - const firstEdge = edges[0]; - const lastEdge = edges[edges.length - 1]; + const startCursor = mappedEdges[0]?.cursor; + const endCursor = mappedEdges[mappedEdges.length - 1]?.cursor; + + const pageInfoKey = getAliasKey({ selectionSet, key: "pageInfo" }); + const edgesKey = getAliasKey({ selectionSet, key: "edges" }); + const pageInfoField = selections.find((x) => x.kind === "Field" && x.name.value === "pageInfo") as FieldNode; + const pageInfoSelectionSet = pageInfoField?.selectionSet; + const startCursorKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "startCursor" }); + const endCursorKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "endCursor" }); + const hasPreviousPageKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "hasPreviousPage" }); + const hasNextPageKey = getAliasKey({ selectionSet: pageInfoSelectionSet, key: "hasNextPage" }); + return { - edges, - pageInfo: { - startCursor: firstEdge?.cursor, - endCursor: lastEdge?.cursor, - hasPreviousPage: lastEdgeCursor > 0, - hasNextPage: typeof first === "number" ? sliceEnd < totalCount : false, + [edgesKey]: mappedEdges, + [pageInfoKey]: { + [startCursorKey]: startCursor, + [endCursorKey]: endCursor, + [hasPreviousPageKey]: lastEdgeCursor > 0, + [hasNextPageKey]: typeof first === "number" ? sliceEnd < totalCount : false, }, }; } diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index c4b362f546..d22d2ade88 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -389,9 +389,15 @@ function createProjectionAndParams({ if (connectionField) { if (!inRelationshipProjection) { - if (!res.meta.connectionFields) res.meta.connectionFields = []; - res.meta.connectionFields.push(field as ResolveTree); - res.projection.push(literalElements ? `${field.name}: ${field.name}` : `${field.name}`); + if (!res.meta.connectionFields) { + res.meta.connectionFields = []; + } + + const f = field as ResolveTree; + + res.meta.connectionFields.push(f); + res.projection.push(literalElements ? `${f.alias}: ${f.alias}` : `${f.alias}`); + return res; } diff --git a/packages/graphql/tests/integration/connections/alias.int.test.ts b/packages/graphql/tests/integration/connections/alias.int.test.ts new file mode 100644 index 0000000000..6bf51dfeec --- /dev/null +++ b/packages/graphql/tests/integration/connections/alias.int.test.ts @@ -0,0 +1,861 @@ +/* + * 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 { Driver } from "neo4j-driver"; +import { graphql } from "graphql"; +import { generate } from "randomstring"; +import neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { gql } from "apollo-server"; + +describe("Connections Alias", () => { + let driver: Driver; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + // using totalCount as the bear minimal selection + test("should alias top level connection field and return correct totalCount", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actors: actorsConnection { + totalCount + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect(result.data as any).toEqual({ + movies: [{ actors: { totalCount: 3 } }], + }); + } finally { + await session.close(); + } + }); + + test("should alias totalCount", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection { + count: totalCount + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect(result.data as any).toEqual({ + movies: [{ actorsConnection: { count: 3 } }], + }); + } finally { + await session.close(); + } + }); + + // using hasNextPage as the bear minimal selection + test("should alias pageInfo top level key", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection { + pi:pageInfo { + hasNextPage + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect(result.data as any).toEqual({ + movies: [{ actorsConnection: { pi: { hasNextPage: false } } }], + }); + } finally { + await session.close(); + } + }); + + test("should alias startCursor", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection { + pageInfo { + sc:startCursor + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.pageInfo.sc).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias endCursor", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection { + pageInfo { + ec:endCursor + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.pageInfo.ec).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias hasPreviousPage", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection { + pageInfo { + hPP:hasPreviousPage + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + CREATE (m)<-[:ACTED_IN]-(:Actor) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.pageInfo.hPP).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias hasNextPage", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + pageInfo { + hNP:hasNextPage + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.pageInfo.hNP).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias the top level edges key", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + e:edges { + cursor + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.e[0].cursor).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias cursor", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + edges { + c:cursor + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.edges[0].c).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias the top level node key", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + edges { + n:node { + name + } + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.edges[0].n).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias a property on the node", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + edges { + node { + n:name + } + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.edges[0].node.n).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias a property on the relationship", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor { + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn { + roles: [String]! + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + actorsConnection(first: 1) { + edges { + r:roles + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:Actor {name: randomUUID()}) + CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:Actor {name: randomUUID()}) + `, + { + movieTitle, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect((result.data as any).movies[0].actorsConnection.edges[0].r).toBeDefined(); + } finally { + await session.close(); + } + }); + + test("should alias many keys on a connection", async () => { + const session = driver.session(); + + const typeDefs = gql` + type Movie { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor { + name: String! + } + + interface ActedIn { + roles: [String]! + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + const roles = [ + generate({ + charset: "alphabetic", + }), + ]; + + const query = ` + { + movies(where: { title: "${movieTitle}" }) { + title + connection:actorsConnection { + tC:totalCount + edges { + n:node { + n:name + } + r:roles + } + page:pageInfo { + hNP:hasNextPage + } + } + } + } + `; + + try { + await session.run( + ` + CREATE (m:Movie {title: $movieTitle}) + CREATE (m)<-[:ACTED_IN {roles: $roles}]-(:Actor {name: $actorName}) + `, + { + movieTitle, + actorName, + roles, + } + ); + + const result = await graphql({ + schema: neoSchema.schema, + source: query, + contextValue: { driver }, + }); + + expect(result.errors).toBeUndefined(); + + expect(result.data as any).toEqual({ + movies: [ + { + title: movieTitle, + connection: { + tC: 1, + edges: [{ n: { n: actorName }, r: roles }], + page: { + hNP: false, + }, + }, + }, + ], + }); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/connections/alias.md b/packages/graphql/tests/tck/tck-test-files/cypher/connections/alias.md new file mode 100644 index 0000000000..c074e4c521 --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/cypher/connections/alias.md @@ -0,0 +1,60 @@ +# Connections Alias + +Schema: + +```graphql +type Movie { + title: String! + actors: [Actor!]! + @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) +} + +type Actor { + name: String! + movies: [Movie!]! + @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) +} + +interface ActedIn { + screenTime: Int! +} +``` + +--- + +## Alias Top Level Connection Field + +### GraphQL Input + +```graphql +{ + movies { + actors: actorsConnection { + totalCount + } + } +} +``` + +### Expected Cypher Output + +```cypher +MATCH (this:Movie) +CALL { + WITH this + MATCH (this)<-[this_acted_in:ACTED_IN]-(this_actor:Actor) + WITH collect({ }) AS edges + RETURN { + totalCount: size(edges) + } AS actors +} +RETURN this { actors } as this +``` + +### Expected Cypher Params + +```json +{} +``` + +---