diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts index 6ad674e615..7d7e5e3b09 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts @@ -40,8 +40,8 @@ import type { EntitySelection } from "../../ast/selection/EntitySelection"; import { NodeSelection } from "../../ast/selection/NodeSelection"; import { RelationshipSelection } from "../../ast/selection/RelationshipSelection"; import { getConcreteEntities } from "../../utils/get-concrete-entities"; -import { isConcreteEntity } from "../../utils/is-concrete-entity"; import { isInterfaceEntity } from "../../utils/is-interface-entity"; +import { isRelationshipEntity } from "../../utils/is-relationship-entity"; import { isUnionEntity } from "../../utils/is-union-entity"; import type { QueryASTFactory } from "../QueryASTFactory"; import { findFieldsByNameInFieldsByTypeNameField } from "../parsers/find-fields-by-name-in-fields-by-type-name-field"; @@ -113,14 +113,14 @@ export class ConnectionFactory { // These sort fields will be duplicated on nested "CompositeConnectionPartial" // TODO: this if shouldn't be needed - if (relationship) { - this.hydrateConnectionOperationsASTWithSort({ - entityOrRel: relationship, - resolveTree, - operation: compositeConnectionOp, - context, - }); - } + // if (relationship) { + this.hydrateConnectionOperationsASTWithSort({ + entityOrRel: relationship ?? target, + resolveTree, + operation: compositeConnectionOp, + context, + }); + // } return compositeConnectionOp; } @@ -183,13 +183,13 @@ export class ConnectionFactory { operation, context, }: { - entityOrRel: ConcreteEntityAdapter | RelationshipAdapter; + entityOrRel: EntityAdapter | RelationshipAdapter; resolveTree: ResolveTree; operation: T; context: Neo4jGraphQLTranslationContext; }): T { let options: Pick | undefined; - const target = isConcreteEntity(entityOrRel) ? entityOrRel : entityOrRel.target; + const target = isRelationshipEntity(entityOrRel) ? entityOrRel.target : entityOrRel; if (!isUnionEntity(target)) { options = this.queryASTFactory.operationsFactory.getConnectionOptions(target, resolveTree.args); } else { diff --git a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts index c6f63da118..02197fb162 100644 --- a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts @@ -20,7 +20,6 @@ import type Cypher from "@neo4j/cypher-builder"; import { SCORE_FIELD } from "../../../graphql/directives/fulltext"; import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; -import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import type { ConnectionSortArg, GraphQLOptionsArg, GraphQLSortArg, NestedGraphQLSortArg } from "../../../types"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; @@ -31,6 +30,7 @@ import { FulltextScoreSort } from "../ast/sort/FulltextScoreSort"; import { PropertySort } from "../ast/sort/PropertySort"; 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"; @@ -52,7 +52,7 @@ export class SortAndPaginationFactory { public createConnectionSortFields( options: ConnectionSortArg, - entityOrRel: ConcreteEntityAdapter | RelationshipAdapter, + entityOrRel: EntityAdapter | RelationshipAdapter, context: Neo4jGraphQLTranslationContext ): { edge: Sort[]; node: Sort[] } { if (isConcreteEntity(entityOrRel)) { @@ -66,14 +66,32 @@ export class SortAndPaginationFactory { node: nodeSortFields, }; } + if (isRelationshipEntity(entityOrRel)) { + const nodeSortFields = this.createPropertySort({ + optionArg: options.node ?? {}, + entity: entityOrRel.target, + context, + }); + const edgeSortFields = this.createPropertySort({ + optionArg: options.edge || {}, + entity: entityOrRel, + context, + }); + return { + edge: edgeSortFields, + node: nodeSortFields, + }; + } + // Is union or interface + const nodeSortFields = this.createPropertySort({ optionArg: options.node ?? {}, - entity: entityOrRel.target, + entity: entityOrRel, context, }); - const edgeSortFields = this.createPropertySort({ optionArg: options.edge || {}, entity: entityOrRel, context }); + return { - edge: edgeSortFields, + edge: [], node: nodeSortFields, }; } diff --git a/packages/graphql/src/translate/queryAST/utils/is-relationship-entity.ts b/packages/graphql/src/translate/queryAST/utils/is-relationship-entity.ts new file mode 100644 index 0000000000..c7ee57c364 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/utils/is-relationship-entity.ts @@ -0,0 +1,25 @@ +/* + * 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 { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; +import { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; + +export function isRelationshipEntity(entity: EntityAdapter | RelationshipAdapter): entity is RelationshipAdapter { + return entity instanceof RelationshipAdapter; +} diff --git a/packages/graphql/tests/integration/connections/interfaces-top-level.int.test.ts b/packages/graphql/tests/integration/connections/interfaces-top-level.int.test.ts new file mode 100644 index 0000000000..23534e8bbc --- /dev/null +++ b/packages/graphql/tests/integration/connections/interfaces-top-level.int.test.ts @@ -0,0 +1,287 @@ +/* + * 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 { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4jHelper from "../neo4j"; + +describe("Interfaces top level connections", () => { + const Series = new UniqueType("Series"); + const Season = new UniqueType("Season"); + const ProgrammeItem = new UniqueType("ProgrammeItem"); + + let schema: GraphQLSchema; + let driver: Driver; + let neo4j: Neo4jHelper; + + async function graphqlQuery(query: string) { + return graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues(), + }); + } + + beforeAll(async () => { + neo4j = new Neo4jHelper(); + driver = await neo4j.getDriver(); + + const typeDefs = /* GraphQL */ ` + interface Show { + title: String! + actors: [Actor!]! @declareRelationship + } + + type Movie implements Show { + title: String! + cost: Float + runtime: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Series implements Show { + title: String! + episodes: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor { + name: String! + actedIn: [Show!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + screenTime: Int + } + `; + const neoGraphql = new Neo4jGraphQL({ typeDefs, driver }); + schema = await neoGraphql.getSchema(); + + await neo4j.run(` + CREATE (m1:Movie {title: "The Matrix", cost: 24}) + CREATE (:Movie {title: "The Godfather", cost: 20}) + CREATE (:Series {title: "The Matrix Series", episodes: 4}) + CREATE (s1:Series {title: "Avatar", episodes: 9}) + + CREATE(a:Actor {name: "Arthur Dent"}) + CREATE(a)-[:ACTED_IN {screenTime: 10}]->(m1) + CREATE(a)-[:ACTED_IN {screenTime: 20}]->(s1) + `); + }); + + afterEach(async () => { + await cleanNodes(driver, [Series, Season, ProgrammeItem]); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("Top level connection page info", async () => { + const query = /* GraphQL */ ` + query { + showsConnection { + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + totalCount + } + } + `; + const queryResults = await graphqlQuery(query); + expect(queryResults.errors).toBeUndefined(); + expect(queryResults.data).toEqual({ + showsConnection: { + pageInfo: { + endCursor: expect.toBeString(), + hasNextPage: false, + hasPreviousPage: false, + startCursor: expect.toBeString(), + }, + totalCount: 4, + }, + }); + }); + + test("Top level connection with filter", async () => { + const query = /* GraphQL */ ` + query { + showsConnection(where: { title_CONTAINS: "The Matrix" }) { + edges { + node { + title + ... on Movie { + cost + } + } + } + } + } + `; + const queryResults = await graphqlQuery(query); + expect(queryResults.errors).toBeUndefined(); + expect(queryResults.data).toEqual({ + showsConnection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Series", + }, + }, + { + node: { + title: "The Matrix", + cost: 24, + }, + }, + ]), + }, + }); + }); + + test("Top level connection with limit", async () => { + const query = /* GraphQL */ ` + query { + showsConnection(where: { title_CONTAINS: "The Matrix" }, first: 1) { + edges { + node { + title + ... on Movie { + cost + } + } + } + } + } + `; + const queryResults = await graphqlQuery(query); + expect(queryResults.errors).toBeUndefined(); + expect(queryResults.data).toEqual({ + showsConnection: { + edges: expect.toBeArrayOfSize(1), + }, + }); + }); + + test("Top level connection with sort and limit DESC", async () => { + const query = /* GraphQL */ ` + query { + showsConnection(where: { title_CONTAINS: "The" }, first: 2, sort: { title: DESC }) { + edges { + node { + title + ... on Movie { + cost + } + } + } + } + } + `; + const queryResults = await graphqlQuery(query); + expect(queryResults.errors).toBeUndefined(); + expect(queryResults.data).toEqual({ + showsConnection: { + edges: [ + { + node: { + title: "The Matrix Series", + }, + }, + { + node: { title: "The Matrix", cost: 24 }, + }, + ], + }, + }); + }); + + test("Top level connection with nested connection", async () => { + const query = /* GraphQL */ ` + query { + showsConnection { + edges { + node { + title + actorsConnection { + edges { + node { + name + } + properties { + ... on ActedIn { + screenTime + } + } + } + } + } + } + } + } + `; + const queryResults = await graphqlQuery(query); + expect(queryResults.errors).toBeUndefined(); + expect(queryResults.data).toEqual({ + showsConnection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Series", + actorsConnection: { + edges: [], + }, + }, + }, + { + node: { + title: "The Matrix", + actorsConnection: { + edges: [{ node: { name: "Arthur Dent" }, properties: { screenTime: 10 } }], + }, + }, + }, + { + node: { + title: "Avatar", + actorsConnection: { + edges: [{ node: { name: "Arthur Dent" }, properties: { screenTime: 20 } }], + }, + }, + }, + { + node: { + title: "The Godfather", + actorsConnection: { + edges: [], + }, + }, + }, + ]), + }, + }); + }); +});