diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 9094f22e30..c5049c21ab 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -1037,6 +1037,8 @@ function makeAugmentedSchema( }); }); } else { + const relatedNode = nodes.find((n) => n.name === connectionField.relationship.typeMeta.name) as Node; + connectionWhere.addFields({ node: `${connectionField.relationship.typeMeta.name}Where`, node_NOT: `${connectionField.relationship.typeMeta.name}Where`, @@ -1044,11 +1046,23 @@ function makeAugmentedSchema( const connectionSort = composer.createInputTC({ name: `${connectionField.typeMeta.name}Sort`, - fields: { - node: `${connectionField.relationship.typeMeta.name}Sort`, - }, + fields: {}, }); + const nodeSortFields = [ + ...relatedNode.primitiveFields, + ...relatedNode.enumFields, + ...relatedNode.scalarFields, + ...relatedNode.dateTimeFields, + ...relatedNode.pointFields, + ].filter((f) => !f.typeMeta.array); + + if (nodeSortFields.length) { + connectionSort.addFields({ + node: `${connectionField.relationship.typeMeta.name}Sort`, + }); + } + if (connectionField.relationship.properties) { connectionSort.addFields({ relationship: `${connectionField.relationship.properties}Sort`, @@ -1057,7 +1071,6 @@ function makeAugmentedSchema( composeNodeArgs = { ...composeNodeArgs, - sort: connectionSort.NonNull.List, first: { type: "Int", }, @@ -1065,6 +1078,14 @@ function makeAugmentedSchema( type: "String", }, }; + + // If any sortable fields, add sort argument to connection field + if (nodeSortFields.length || connectionField.relationship.properties) { + composeNodeArgs = { + ...composeNodeArgs, + sort: connectionSort.NonNull.List, + }; + } } composeNode.addFields({ diff --git a/packages/graphql/tests/integration/issues/#288.int.test.ts b/packages/graphql/tests/integration/issues/#288.int.test.ts new file mode 100644 index 0000000000..aba93b57e6 --- /dev/null +++ b/packages/graphql/tests/integration/issues/#288.int.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { gql } from "apollo-server"; +import { generate } from "randomstring"; +import neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; + +describe("https://github.com/neo4j/graphql/issues/288", () => { + let driver: Driver; + const typeDefs = gql` + type USER { + USERID: String + COMPANYID: String + COMPANY: [COMPANY] @relationship(type: "IS_PART_OF", direction: OUT) + } + + type COMPANY { + USERS: [USER] @relationship(type: "IS_PART_OF", direction: IN) + } + `; + + beforeAll(async () => { + driver = await neo4j(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("COMPANYID can be populated on create and update", async () => { + const session = driver.session(); + + const neoSchema = new Neo4jGraphQL({ typeDefs, driver }); + + const userid = generate({ charset: "alphabetic" }); + const companyid1 = generate({ charset: "alphabetic" }); + const companyid2 = generate({ charset: "alphabetic" }); + + const createMutation = ` + mutation { + createUSERS(input: { USERID: "${userid}", COMPANYID: "${companyid1}" }) { + users { + USERID + COMPANYID + } + } + } + `; + + const updateMutation = ` + mutation { + updateUSERS(where: { USERID: "${userid}" }, update: { COMPANYID: "${companyid2}" }) { + users { + USERID + COMPANYID + } + } + } + `; + + try { + await neoSchema.checkNeo4jCompat(); + + const createResult = await graphql({ + schema: neoSchema.schema, + source: createMutation, + contextValue: { driver }, + }); + + expect(createResult.errors).toBeFalsy(); + + expect(createResult?.data?.createUSERS?.users).toEqual([{ USERID: userid, COMPANYID: companyid1 }]); + + const updateResult = await graphql({ + schema: neoSchema.schema, + source: updateMutation, + contextValue: { driver }, + }); + + expect(updateResult.errors).toBeFalsy(); + + expect(updateResult?.data?.updateUSERS?.users).toEqual([{ USERID: userid, COMPANYID: companyid2 }]); + + await session.run(`MATCH (u:USER) WHERE u.USERID = "${userid}" DELETE u`); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/tck/tck-test-files/cypher/issues/#288.md b/packages/graphql/tests/tck/tck-test-files/cypher/issues/#288.md new file mode 100644 index 0000000000..cc2b58bde9 --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/cypher/issues/#288.md @@ -0,0 +1,99 @@ +## #288 + + + +Type definitions: + +```schema +type USER { + USERID: String + COMPANYID: String + COMPANY: [COMPANY] @relationship(type: "IS_PART_OF", direction: OUT) +} + +type COMPANY { + USERS: [USER] @relationship(type: "IS_PART_OF", direction: IN) +} +``` + +--- + +### Can create a USER and COMPANYID is populated + +**GraphQL input** + +```graphql +mutation { + createUSERS(input: { USERID: "userid", COMPANYID: "companyid" }) { + users { + USERID + COMPANYID + } + } +} +``` + +**Expected Cypher output** + +```cypher +CALL { + CREATE (this0:USER) + SET this0.USERID = $this0_USERID + SET this0.COMPANYID = $this0_COMPANYID + RETURN this0 +} + +RETURN +this0 { .USERID, .COMPANYID } AS this0 +``` + +**Expected Cypher params** + +```cypher-params +{ + "this0_USERID": "userid", + "this0_COMPANYID": "companyid" +} +``` + +--- + +### Can update a USER and COMPANYID is populated + +**GraphQL input** + +```graphql +mutation { + updateUSERS( + where: { USERID: "userid" } + update: { COMPANYID: "companyid2" } + ) { + users { + USERID + COMPANYID + } + } +} +``` + +**Expected Cypher output** + +```cypher +MATCH (this:USER) +WHERE this.USERID = $this_USERID + +SET this.COMPANYID = $this_update_COMPANYID + +RETURN this { .USERID, .COMPANYID } AS this +``` + +**Expected Cypher params** + +```cypher-params +{ + "this_USERID": "userid", + "this_update_COMPANYID": "companyid2" +} +``` + +--- diff --git a/packages/graphql/tests/tck/tck-test-files/schema/connections/sort.md b/packages/graphql/tests/tck/tck-test-files/schema/connections/sort.md new file mode 100644 index 0000000000..55e677f3e0 --- /dev/null +++ b/packages/graphql/tests/tck/tck-test-files/schema/connections/sort.md @@ -0,0 +1,324 @@ +## Sort + +Tests sort argument on connection fields + +--- + +### sort argument is not present when nothing to sort + +**TypeDefs** + +```typedefs-input +type Node1 { + property: String! + relatedTo: [Node2!]! @relationship(type: "RELATED_TO", direction: OUT) +} + +type Node2 { + relatedTo: [Node1!]! @relationship(type: "RELATED_TO", direction: OUT) +} +``` + +**Output** + +```schema-output +type CreateNode1sMutationResponse { + node1s: [Node1!]! +} + +type CreateNode2sMutationResponse { + node2s: [Node2!]! +} + +type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! +} + +type Mutation { + createNode1s(input: [Node1CreateInput!]!): CreateNode1sMutationResponse! + deleteNode1s(where: Node1Where, delete: Node1DeleteInput): DeleteInfo! + updateNode1s( + where: Node1Where + update: Node1UpdateInput + connect: Node1ConnectInput + disconnect: Node1DisconnectInput + create: Node1RelationInput + delete: Node1DeleteInput + ): UpdateNode1sMutationResponse! + createNode2s(input: [Node2CreateInput!]!): CreateNode2sMutationResponse! + deleteNode2s(where: Node2Where, delete: Node2DeleteInput): DeleteInfo! + updateNode2s( + where: Node2Where + update: Node2UpdateInput + connect: Node2ConnectInput + disconnect: Node2DisconnectInput + create: Node2RelationInput + delete: Node2DeleteInput + ): UpdateNode2sMutationResponse! +} + +type Node1 { + property: String! + relatedTo(where: Node2Where, options: Node2Options): [Node2!]! + relatedToConnection( + where: Node1RelatedToConnectionWhere + first: Int + after: String + ): Node1RelatedToConnection! +} + +input Node1ConnectInput { + relatedTo: [Node1RelatedToConnectFieldInput!] +} + +input Node1ConnectWhere { + node: Node1Where! +} + +input Node1CreateInput { + property: String! + relatedTo: Node1RelatedToFieldInput +} + +input Node1DeleteInput { + relatedTo: [Node1RelatedToDeleteFieldInput!] +} + +input Node1DisconnectInput { + relatedTo: [Node1RelatedToDisconnectFieldInput!] +} + +input Node1Options { + # Specify one or more Node1Sort objects to sort Node1s by. The sorts will be applied in the order in which they are arranged in the array. + sort: [Node1Sort] + limit: Int + offset: Int +} + +input Node1RelatedToConnectFieldInput { + where: Node2ConnectWhere + connect: [Node2ConnectInput!] +} + +type Node1RelatedToConnection { + edges: [Node1RelatedToRelationship!]! + totalCount: Int! + pageInfo: PageInfo! +} + +input Node1RelatedToConnectionWhere { + AND: [Node1RelatedToConnectionWhere!] + OR: [Node1RelatedToConnectionWhere!] + node: Node2Where + node_NOT: Node2Where +} + +input Node1RelatedToCreateFieldInput { + node: Node2CreateInput! +} + +input Node1RelatedToDeleteFieldInput { + where: Node1RelatedToConnectionWhere + delete: Node2DeleteInput +} + +input Node1RelatedToDisconnectFieldInput { + where: Node1RelatedToConnectionWhere + disconnect: Node2DisconnectInput +} + +input Node1RelatedToFieldInput { + create: [Node1RelatedToCreateFieldInput!] + connect: [Node1RelatedToConnectFieldInput!] +} + +type Node1RelatedToRelationship { + cursor: String! + node: Node2! +} + +input Node1RelatedToUpdateConnectionInput { + node: Node2UpdateInput +} + +input Node1RelatedToUpdateFieldInput { + where: Node1RelatedToConnectionWhere + update: Node1RelatedToUpdateConnectionInput + connect: [Node1RelatedToConnectFieldInput!] + disconnect: [Node1RelatedToDisconnectFieldInput!] + create: [Node1RelatedToCreateFieldInput!] + delete: [Node1RelatedToDeleteFieldInput!] +} + +input Node1RelationInput { + relatedTo: [Node1RelatedToCreateFieldInput!] +} + +# Fields to sort Node1s by. The order in which sorts are applied is not guaranteed when specifying many fields in one Node1Sort object. +input Node1Sort { + property: SortDirection +} + +input Node1UpdateInput { + property: String + relatedTo: [Node1RelatedToUpdateFieldInput!] +} + +input Node1Where { + OR: [Node1Where!] + AND: [Node1Where!] + property: String + property_NOT: String + property_IN: [String] + property_NOT_IN: [String] + property_CONTAINS: String + property_NOT_CONTAINS: String + property_STARTS_WITH: String + property_NOT_STARTS_WITH: String + property_ENDS_WITH: String + property_NOT_ENDS_WITH: String + relatedTo: Node2Where + relatedTo_NOT: Node2Where +} + +type Node2 { + relatedTo(where: Node1Where, options: Node1Options): [Node1!]! + relatedToConnection( + where: Node2RelatedToConnectionWhere + first: Int + after: String + sort: [Node2RelatedToConnectionSort!] + ): Node2RelatedToConnection! +} + +input Node2ConnectInput { + relatedTo: [Node2RelatedToConnectFieldInput!] +} + +input Node2ConnectWhere { + node: Node2Where! +} + +input Node2CreateInput { + relatedTo: Node2RelatedToFieldInput +} + +input Node2DeleteInput { + relatedTo: [Node2RelatedToDeleteFieldInput!] +} + +input Node2DisconnectInput { + relatedTo: [Node2RelatedToDisconnectFieldInput!] +} + +input Node2Options { + limit: Int + offset: Int +} + +input Node2RelatedToConnectFieldInput { + where: Node1ConnectWhere + connect: [Node1ConnectInput!] +} + +type Node2RelatedToConnection { + edges: [Node2RelatedToRelationship!]! + totalCount: Int! + pageInfo: PageInfo! +} + +input Node2RelatedToConnectionSort { + node: Node1Sort +} + +input Node2RelatedToConnectionWhere { + AND: [Node2RelatedToConnectionWhere!] + OR: [Node2RelatedToConnectionWhere!] + node: Node1Where + node_NOT: Node1Where +} + +input Node2RelatedToCreateFieldInput { + node: Node1CreateInput! +} + +input Node2RelatedToDeleteFieldInput { + where: Node2RelatedToConnectionWhere + delete: Node1DeleteInput +} + +input Node2RelatedToDisconnectFieldInput { + where: Node2RelatedToConnectionWhere + disconnect: Node1DisconnectInput +} + +input Node2RelatedToFieldInput { + create: [Node2RelatedToCreateFieldInput!] + connect: [Node2RelatedToConnectFieldInput!] +} + +type Node2RelatedToRelationship { + cursor: String! + node: Node1! +} + +input Node2RelatedToUpdateConnectionInput { + node: Node1UpdateInput +} + +input Node2RelatedToUpdateFieldInput { + where: Node2RelatedToConnectionWhere + update: Node2RelatedToUpdateConnectionInput + connect: [Node2RelatedToConnectFieldInput!] + disconnect: [Node2RelatedToDisconnectFieldInput!] + create: [Node2RelatedToCreateFieldInput!] + delete: [Node2RelatedToDeleteFieldInput!] +} + +input Node2RelationInput { + relatedTo: [Node2RelatedToCreateFieldInput!] +} + +input Node2UpdateInput { + relatedTo: [Node2RelatedToUpdateFieldInput!] +} + +input Node2Where { + OR: [Node2Where!] + AND: [Node2Where!] + relatedTo: Node1Where + relatedTo_NOT: Node1Where +} + +# Pagination information (Relay) +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String! + endCursor: String! +} + +type Query { + node1s(where: Node1Where, options: Node1Options): [Node1!]! + node2s(where: Node2Where, options: Node2Options): [Node2!]! +} + +enum SortDirection { + # Sort by field values in ascending order. + ASC + + # Sort by field values in descending order. + DESC +} + +type UpdateNode1sMutationResponse { + node1s: [Node1!]! +} + +type UpdateNode2sMutationResponse { + node2s: [Node2!]! +} +``` + +---