diff --git a/.changeset/dry-doors-beg.md b/.changeset/dry-doors-beg.md new file mode 100644 index 0000000000..bd760d7b0d --- /dev/null +++ b/.changeset/dry-doors-beg.md @@ -0,0 +1,6 @@ +--- +"@neo4j/introspector": major +"@neo4j/graphql": major +--- + +Deprecated @node directive arguments `label` and `additionalLabels` have been removed. Please use the `labels` argument. diff --git a/docs/modules/ROOT/pages/type-definitions/database-mapping.adoc b/docs/modules/ROOT/pages/type-definitions/database-mapping.adoc index 22375f7de0..9b5079e2e6 100644 --- a/docs/modules/ROOT/pages/type-definitions/database-mapping.adoc +++ b/docs/modules/ROOT/pages/type-definitions/database-mapping.adoc @@ -82,12 +82,6 @@ The `@node` directive is used to specify the configuration of a GraphQL object t directive @node( """Map the GraphQL type to match Neo4j node labels""" labels: [String] - """Map the GraphQL type to a custom Neo4j node label""" - label: String - """Map the GraphQL type to match additional Neo4j node labels""" - additionalLabels: [String] - """Allows for the specification of the plural of the type name.""" - plural: String ) on OBJECT ---- @@ -152,45 +146,12 @@ RETURN this { .name } as this ---- -==== `label` - -NOTE: The label and additionalLabels arguments of the `@node` directive have been deprecated and will be removed in version 4.0. -Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. - -The parameter `label` defines the label to be used in Neo4j instead of the GraphQL type name: - -[source, graphql, indent=0] ----- -type Movie @node(label: "Film") { - title: String! -} ----- - -This way, the following query: - -[source, graphql, indent=0] ----- -{ - movies { - title - } -} ----- - -Generates the cypher query: - -[source, cypher, indent=0] ----- -MATCH (this: Film) -RETURN this { .title } as this ----- - ===== Using `$jwt` and `$context` In some cases, we may want to generate dynamic labels depending on the user requesting. In these cases, we can use the variable `$jwt` to define a custom label define in the JWT (similarly to how it is used in the xref::auth/index.adoc[`@auth` directive]): [source, graphql, indent=0] ---- -type User @node(label: "$jwt.username") { +type User @node(labels: ["$jwt.username"]) { name: String! } ---- @@ -218,7 +179,7 @@ Similarly, context values can be passed directly: [source, graphql, indent=0] ---- -type User @node(label: "$context.appId") { +type User @node(label: ["$context.appId"]) { name: String! } ---- @@ -234,85 +195,3 @@ neoSchema.getSchema().then((schema) => { }); }) ---- - -==== `additionalLabels` - -NOTE: The label and additionalLabels arguments of the `@node` directive have been deprecated and will be removed in version 4.0. -Please use the xref::type-definitions/database-mapping.adoc#_labels[`labels argument`] instead. - -`additionalLabels` lets you define extra Neo4j labels that need to exist on the node for that GraphQL type. - -[source, graphql, indent=0] ----- -type Actor @node(additionalLabels: ["Person", "User"]) { - name: String! -} ----- - -The following query: - -[source, graphql, indent=0] ----- -{ - Actor { - name - } -} ----- - -Generates the following cypher query, with the labels `Actor`, `Person` and `User`: - -[source, cypher, indent=0] ----- -MATCH (this:Actor:Person:User) -RETURN this { .name } as this ----- - -Note that `additionalLabels` can be used along with `label`: - -[source, graphql, indent=0] ----- -type Actor @node(label: "ActorDB", additionalLabels: ["Person"]) { - name: String! -} ----- - -In this case, the resulting Cypher query will use the labels `ActorDB` and `Person` instead of `Actor`: - ----- -MATCH (this:ActorDB:Person) -RETURN this { .name } as this ----- -<<#_using_jwt_and_context,Context and JWT variables>> can be used with `additionalLabels` in the same fashion as in `label`: - -[source, graphql, indent=0] ----- -type User @node(additionalLabels: ["$jwt.username"]) { - name: String! -} ----- - -==== `plural` - -The parameter `plural` redefines how to compose the plural of the type for the generated operations. This is particularly -useful for types that are not correctly pluralized or are non-English words. - -[source, graphql, indent=0] ----- -type Tech @node(plural: "Techs") { - name: String -} ----- - -This way, instead of the wrongly generated `teches`, the type is properly written as `techs`: - -[source, graphql, indent=0] ----- -{ - techs { - title - } -} ----- - -The same is applied to other operations such as `createTechs`. Note that database labels will not change. diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index e1a06a0ed2..4841f95f44 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -304,18 +304,10 @@ class Node extends GraphElement { } public getMainLabel(): string { - return this.nodeDirective?.labels?.[0] || this.nodeDirective?.label || this.name; + return this.nodeDirective?.labels?.[0] || this.name; } public getAllLabels(): string[] { - if (!this.nodeDirective) { - return [this.name]; - } - if (this.nodeDirective.labels.length) { - return this.nodeDirective.labels; - } - return [this.nodeDirective.label || this.name, ...(this.nodeDirective.additionalLabels || [])]; - // TODO: use when removing label & additionalLabels - // return this.nodeDirective?.labels || [this.name]; + return this.nodeDirective?.labels || [this.name]; } public getGlobalIdField(): string { diff --git a/packages/graphql/src/classes/NodeDirective.ts b/packages/graphql/src/classes/NodeDirective.ts index 1865c828ba..2b6beeccd6 100644 --- a/packages/graphql/src/classes/NodeDirective.ts +++ b/packages/graphql/src/classes/NodeDirective.ts @@ -23,19 +23,13 @@ import ContextParser from "../utils/context-parser"; import Cypher from "@neo4j/cypher-builder"; export interface NodeDirectiveConstructor { - label?: string; - additionalLabels?: string[]; labels?: string[]; } export class NodeDirective { - public readonly label: string | undefined; - public readonly additionalLabels: string[]; public readonly labels: string[]; constructor(input: NodeDirectiveConstructor) { - this.label = input.label; - this.additionalLabels = input.additionalLabels || []; this.labels = input.labels || []; } @@ -48,15 +42,7 @@ export class NodeDirective { } public getLabels(typeName: string, context: Context): string[] { - let labels: string[] = []; - if (this.labels.length) { - labels = [...this.labels]; - } else { - const mainLabel = this.label || typeName; - labels = [mainLabel, ...this.additionalLabels]; - } - // TODO: use when removing label & additionalLabels - // const labels = !this.labels.length ? [typeName] : this.labels; + const labels = !this.labels.length ? [typeName] : this.labels; return this.mapLabelsWithContext(labels, context); } diff --git a/packages/graphql/src/classes/deprecated/Node.test.ts b/packages/graphql/src/classes/deprecated/Node.test.ts deleted file mode 100644 index bd18887e99..0000000000 --- a/packages/graphql/src/classes/deprecated/Node.test.ts +++ /dev/null @@ -1,948 +0,0 @@ -/* - * 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 { MutationResponseTypeNames, NodeConstructor, RootTypeFieldNames, SubscriptionEvents } from "../Node"; -import Node from "../Node"; -import { ContextBuilder } from "../../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../../tests/utils/builders/node-builder"; -import { NodeDirective } from "../NodeDirective"; - -describe("Node", () => { - const defaultContext = new ContextBuilder().instance(); - - test("should construct", () => { - // @ts-ignore - const input: NodeConstructor = { - name: "Movie", - cypherFields: [], - enumFields: [], - primitiveFields: [], - scalarFields: [], - temporalFields: [], - unionFields: [], - interfaceFields: [], - objectFields: [], - interfaces: [], - otherDirectives: [], - pointFields: [], - relationFields: [], - }; - - // @ts-ignore - expect(new Node(input)).toMatchObject({ name: "Movie" }); - }); - - test("should return labelString from node name", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); - - expect(node.getLabelString(defaultContext)).toBe(":Movie"); - }); - - test("should return labels from node name", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); - - expect(node.getLabels(defaultContext)).toEqual(["Movie"]); - }); - - describe("root type field names", () => { - test.each<[string, RootTypeFieldNames]>([ - [ - "Account", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - relationship_created: "accountRelationshipCreated", - relationship_deleted: "accountRelationshipDeleted", - }, - }, - ], - [ - "AWSAccount", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - relationship_created: "awsAccountRelationshipCreated", - relationship_deleted: "awsAccountRelationshipDeleted", - }, - }, - ], - [ - "AWS_ACCOUNT", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - relationship_created: "awsAccountRelationshipCreated", - relationship_deleted: "awsAccountRelationshipDeleted", - }, - }, - ], - [ - "aws-account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - relationship_created: "awsAccountRelationshipCreated", - relationship_deleted: "awsAccountRelationshipDeleted", - }, - }, - ], - [ - "aws_account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - relationship_created: "awsAccountRelationshipCreated", - relationship_deleted: "awsAccountRelationshipDeleted", - }, - }, - ], - [ - "account", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - relationship_created: "accountRelationshipCreated", - relationship_deleted: "accountRelationshipDeleted", - }, - }, - ], - [ - "ACCOUNT", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - relationship_created: "accountRelationshipCreated", - relationship_deleted: "accountRelationshipDeleted", - }, - }, - ], - [ - "A", - { - create: "createAs", - read: "as", - update: "updateAs", - delete: "deleteAs", - aggregate: "asAggregate", - subscribe: { - created: "aCreated", - updated: "aUpdated", - deleted: "aDeleted", - relationship_created: "aRelationshipCreated", - relationship_deleted: "aRelationshipDeleted", - }, - }, - ], - [ - "_2number", - { - create: "create_2Numbers", - read: "_2Numbers", - update: "update_2Numbers", - delete: "delete_2Numbers", - aggregate: "_2NumbersAggregate", - subscribe: { - created: "_2NumberCreated", - updated: "_2NumberUpdated", - deleted: "_2NumberDeleted", - relationship_created: "_2NumberRelationshipCreated", - relationship_deleted: "_2NumberRelationshipDeleted", - }, - }, - ], - [ - "__2number", - { - create: "create__2Numbers", - read: "__2Numbers", - update: "update__2Numbers", - delete: "delete__2Numbers", - aggregate: "__2NumbersAggregate", - subscribe: { - created: "__2NumberCreated", - updated: "__2NumberUpdated", - deleted: "__2NumberDeleted", - relationship_created: "__2NumberRelationshipCreated", - relationship_deleted: "__2NumberRelationshipDeleted", - }, - }, - ], - [ - "_number", - { - create: "create_numbers", - read: "_numbers", - update: "update_numbers", - delete: "delete_numbers", - aggregate: "_numbersAggregate", - subscribe: { - created: "_numberCreated", - updated: "_numberUpdated", - deleted: "_numberDeleted", - relationship_created: "_numberRelationshipCreated", - relationship_deleted: "_numberRelationshipDeleted", - }, - }, - ], - ])("should pluralize %s as expected", (typename: string, rootTypeFieldNames: RootTypeFieldNames) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.rootTypeFieldNames).toStrictEqual(rootTypeFieldNames); - }); - - test.each<[string, RootTypeFieldNames]>([ - [ - "Accounts", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "AWSAccounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "AWS_ACCOUNTS", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "aws-accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "aws_accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "accounts", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - [ - "ACCOUNTS", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - relationship_created: "testRelationshipCreated", - relationship_deleted: "testRelationshipDeleted", - }, - }, - ], - ])( - "should pluralize %s as expected with plural specified in @node directive", - (plural: string, rootTypeFieldNames: RootTypeFieldNames) => { - const node = new NodeBuilder({ - name: "Test", - nodeDirective: new NodeDirective({ plural }), - }).instance(); - - expect(node.rootTypeFieldNames).toStrictEqual(rootTypeFieldNames); - } - ); - }); - - describe("mutation response type names", () => { - test.each<[string, MutationResponseTypeNames]>([ - [ - "Account", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "AWSAccount", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws-account", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws_account", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "account", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "ACCOUNT", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "A", - { - create: "CreateAsMutationResponse", - update: "UpdateAsMutationResponse", - }, - ], - [ - "_2number", - { - create: "Create_2NumbersMutationResponse", - update: "Update_2NumbersMutationResponse", - }, - ], - [ - "__2number", - { - create: "Create__2NumbersMutationResponse", - update: "Update__2NumbersMutationResponse", - }, - ], - [ - "_number", - { - create: "Create_numbersMutationResponse", - update: "Update_numbersMutationResponse", - }, - ], - ])( - "should generate mutation response type names for %s as expected", - (typename: string, mutationResponseTypeNames: MutationResponseTypeNames) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.mutationResponseTypeNames).toStrictEqual(mutationResponseTypeNames); - } - ); - - test.each<[string, MutationResponseTypeNames]>([ - [ - "Accounts", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "AWSAccounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "AWS_ACCOUNTS", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws-accounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws_accounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "accounts", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "ACCOUNTS", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - ])( - "should generate mutation response type names for %s as expected with plural specified in @node directive", - (plural: string, mutationResponseTypeNames: MutationResponseTypeNames) => { - const node = new NodeBuilder({ - name: "Test", - nodeDirective: new NodeDirective({ plural }), - }).instance(); - - expect(node.mutationResponseTypeNames).toStrictEqual(mutationResponseTypeNames); - } - ); - }); - - describe("Subscription event type names", () => { - test.each<[string, SubscriptionEvents]>([ - [ - "Account", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "AWSAccount", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "aws-account", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "aws_account", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "account", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "ACCOUNT", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "A", - { - create: "ACreatedEvent", - update: "AUpdatedEvent", - delete: "ADeletedEvent", - create_relationship: "ARelationshipCreatedEvent", - delete_relationship: "ARelationshipDeletedEvent", - }, - ], - [ - "_2number", - { - create: "_2NumberCreatedEvent", - update: "_2NumberUpdatedEvent", - delete: "_2NumberDeletedEvent", - create_relationship: "_2NumberRelationshipCreatedEvent", - delete_relationship: "_2NumberRelationshipDeletedEvent", - }, - ], - [ - "__2number", - { - create: "__2NumberCreatedEvent", - update: "__2NumberUpdatedEvent", - delete: "__2NumberDeletedEvent", - create_relationship: "__2NumberRelationshipCreatedEvent", - delete_relationship: "__2NumberRelationshipDeletedEvent", - }, - ], - [ - "_number", - { - create: "_numberCreatedEvent", - update: "_numberUpdatedEvent", - delete: "_numberDeletedEvent", - create_relationship: "_numberRelationshipCreatedEvent", - delete_relationship: "_numberRelationshipDeletedEvent", - }, - ], - ])( - "should generate Subscription event type names for %s as expected", - (typename: string, subscriptionEventTypeNames: SubscriptionEvents) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.subscriptionEventTypeNames).toStrictEqual(subscriptionEventTypeNames); - } - ); - }); - - describe("Subscription event payload field names", () => { - test.each<[string, SubscriptionEvents]>([ - [ - "Account", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "AWSAccount", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "aws-account", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "aws_account", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "account", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "ACCOUNT", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "A", - { - create: "createdA", - update: "updatedA", - delete: "deletedA", - create_relationship: "a", - delete_relationship: "a", - }, - ], - [ - "_2number", - { - create: "created_2Number", - update: "updated_2Number", - delete: "deleted_2Number", - create_relationship: "_2Number", - delete_relationship: "_2Number", - }, - ], - [ - "__2number", - { - create: "created__2Number", - update: "updated__2Number", - delete: "deleted__2Number", - create_relationship: "__2Number", - delete_relationship: "__2Number", - }, - ], - [ - "_number", - { - create: "created_number", - update: "updated_number", - delete: "deleted_number", - create_relationship: "_number", - delete_relationship: "_number", - }, - ], - ])( - "should generate Subscription event type names for %s as expected", - (typename: string, subscriptionEventPayloadFieldNames: SubscriptionEvents) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.subscriptionEventPayloadFieldNames).toStrictEqual(subscriptionEventPayloadFieldNames); - } - ); - }); - - describe("global node resolution", () => { - test("should return true if it is a global node", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "dbId", - }).instance(); - - const isGlobalNode = node.isGlobalNode; - expect(isGlobalNode).toBe(true); - }); - - test("should convert the db id to a global relay id with the correct typename", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "title", - }).instance(); - - const value = "the Matrix"; - - const relayId = node.toGlobalId(value); - - expect(relayId).toBe("RmlsbTp0aXRsZTp0aGUgTWF0cml4"); - - expect(node.fromGlobalId(relayId)).toEqual({ - typeName: "Film", - field: "title", - id: value, - }); - }); - test("should properly convert a relay id to an object when the id has a colon in the name", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "title", - }).instance(); - - const value = "2001: A Space Odyssey"; - - const relayId = node.toGlobalId(value); - - expect(node.fromGlobalId(relayId)).toMatchObject({ field: "title", typeName: "Film", id: value }); - }); - }); - - describe("NodeDirective", () => { - test("should return labels updated with jwt values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - label: "$jwt.movielabel", - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - myKey: "key", - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Movie"]); - expect(labelString).toBe(":`Movie`"); - }); - - test("should return labels updated with context values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - label: "$context.myKey", - }) - .instance(); - - const context = new ContextBuilder() - .with({ - myKey: "Movie", - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Movie"]); - expect(labelString).toBe(":`Movie`"); - }); - - test("should return additional labels updated with jwt values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - label: "Film", - additionalLabels: ["$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - myKey: "key", - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Film", "Movie"]); - expect(labelString).toBe(":`Film`:`Movie`"); - }); - - test("should prefer labels over additional labels", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - labels: ["Something"], - label: "Film", - additionalLabels: ["$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - myKey: "key", - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Something"]); - expect(labelString).toBe(":`Something`"); - }); - }); -}); diff --git a/packages/graphql/src/classes/deprecated/NodeDirective.test.ts b/packages/graphql/src/classes/deprecated/NodeDirective.test.ts deleted file mode 100644 index 13fc00373e..0000000000 --- a/packages/graphql/src/classes/deprecated/NodeDirective.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { NodeDirective } from "../NodeDirective"; -import { ContextBuilder } from "../../../tests/utils/builders/context-builder"; - -describe("NodeDirective", () => { - const defaultContext = new ContextBuilder().instance(); - - test("should generate label string with only the input typename", () => { - const instance = new NodeDirective({}); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":`MyLabel`"); - }); - - test("should generate label string with directive label", () => { - const instance = new NodeDirective({ label: "MyOtherLabel" }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":`MyOtherLabel`"); - }); - - test("should generate label string adding additional labels to input typename", () => { - const instance = new NodeDirective({ additionalLabels: ["Label1", "Label2"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":`MyLabel`:`Label1`:`Label2`"); - }); - - test("should generate label string adding additional labels to directive label", () => { - const instance = new NodeDirective({ label: "MyOtherLabel", additionalLabels: ["Label1", "Label2"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":`MyOtherLabel`:`Label1`:`Label2`"); - }); - - test("should throw an error if there are no labels", () => { - const instance = new NodeDirective({}); - expect(() => { - instance.getLabelsString("", defaultContext); - }).toThrow(); - }); - - test("should escape context labels", () => { - const context = new ContextBuilder({ escapeTest1: "123-321", escapeTest2: "He`l`lo" }).instance(); - const instance = new NodeDirective({ - additionalLabels: ["$context.escapeTest1", "$context.escapeTest2"], - }); - const labelString = instance.getLabelsString("label", context); - expect(labelString).toBe(":`label`:`123-321`:`He``l``lo`"); - }); - - test("should escape jwt labels", () => { - const context = new ContextBuilder({ jwt: { escapeTest1: "123-321", escapeTest2: "He`l`lo" } }).instance(); - const instance = new NodeDirective({ - additionalLabels: ["$jwt.escapeTest1", "$jwt.escapeTest2"], - }); - const labelString = instance.getLabelsString("label", context); - expect(labelString).toBe(":`label`:`123-321`:`He``l``lo`"); - }); - - test("should throw if jwt variable is missing in context", () => { - const context = new ContextBuilder({}).instance(); - const instance = new NodeDirective({ - additionalLabels: ["$jwt.var1"], - }); - expect(() => { - instance.getLabelsString("label", context); - }).toThrow("Label value not found in context."); - }); - - test("should throw if context variable is missing in context", () => { - const context = new ContextBuilder({}).instance(); - const instance = new NodeDirective({ - additionalLabels: ["$context.var1"], - }); - expect(() => { - instance.getLabelsString("label", context); - }).toThrow("Label value not found in context."); - }); - - test("should prefer labels over additional labels + label", () => { - const instance = new NodeDirective({ labels: ["Something"], additionalLabels: ["Label1", "Label2"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":`Something`"); - }); -}); diff --git a/packages/graphql/src/graphql/directives/node.ts b/packages/graphql/src/graphql/directives/node.ts index 2b91369628..f0c985d1a9 100644 --- a/packages/graphql/src/graphql/directives/node.ts +++ b/packages/graphql/src/graphql/directives/node.ts @@ -19,34 +19,11 @@ import { DirectiveLocation, GraphQLDirective, GraphQLList, GraphQLNonNull, GraphQLString } from "graphql"; -const labelDescription = - "NOTE: The label argument has been deprecated and will be removed in version 4.0.0. " + - "Please use the labels argument instead. More information can be found at " + - "https://neo4j.com/docs/graphql-manual/current/guides/v4-migration/" + - "#_label_and_additionalLabels_arguments_removed_from_node_and_replaced_with_new_argument_labels. "; -const additionalLabelsDescription = - "NOTE: The additionalLabels argument has been deprecated and will be removed in version 4.0.0. " + - "Please use the labels argument instead. " + - "If not used in conjunction with the also deprecated label argument, make sure to specify the GraphQL node type as first item in the array." + - "More information can be found at " + - "https://neo4j.com/docs/graphql-manual/current/guides/v4-migration/" + - "#_label_and_additionalLabels_arguments_removed_from_node_and_replaced_with_new_argument_labels. "; - export const nodeDirective = new GraphQLDirective({ name: "node", description: "Informs @neo4j/graphql of node metadata", locations: [DirectiveLocation.OBJECT], args: { - label: { - description: "Map the GraphQL type to a custom Neo4j node label.", - type: GraphQLString, - deprecationReason: labelDescription, - }, - additionalLabels: { - description: "Map the GraphQL type to match additional Neo4j node labels.", - type: new GraphQLList(new GraphQLNonNull(GraphQLString)), - deprecationReason: additionalLabelsDescription, - }, labels: { description: "The labels to map this GraphQL type to in the Neo4j database", type: new GraphQLList(new GraphQLNonNull(GraphQLString)), diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 1b398fddf7..94545cba15 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -93,16 +93,10 @@ function generateConcreteEntity(definition: ObjectTypeDefinitionNode): ConcreteE } function getLabels(definition: ObjectTypeDefinitionNode, nodeDirectiveArguments: Record): string[] { - // TODO: use when removing label & additionalLabels - // const nodeExplicitLabels = nodeDirectiveArguments.labels as string[]; - // return nodeExplicitLabels ?? [definition.name.value]; if ((nodeDirectiveArguments.labels as string[] | undefined)?.length) { return nodeDirectiveArguments.labels as string[]; } - const nodeLabel = nodeDirectiveArguments.label as string | undefined; - const additionalLabels = (nodeDirectiveArguments.additionalLabels || []) as string[]; - const label = nodeLabel || definition.name.value; - return [label, ...additionalLabels]; + return [definition.name.value]; } function generateField(field: FieldDefinitionNode): Attribute | undefined { diff --git a/packages/graphql/src/schema/deprecated/parse-node-directive.test.ts b/packages/graphql/src/schema/deprecated/parse-node-directive.test.ts deleted file mode 100644 index 5045e08e35..0000000000 --- a/packages/graphql/src/schema/deprecated/parse-node-directive.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 { DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import { parse } from "graphql"; -import parseNodeDirective from "../parse-node-directive"; -import { NodeDirective } from "../../classes/NodeDirective"; - -describe("parseNodeDirective", () => { - test("should throw an error if incorrect directive is passed in", () => { - const typeDefs = ` - type TestType @wrongdirective { - label: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - expect(() => parseNodeDirective(directive)).toThrow( - "Undefined or incorrect directive passed into parseNodeDirective function" - ); - }); - - test("should return a node directive with a label", () => { - const typeDefs = ` - type TestType @node(label:"MyLabel") { - name: String - } - `; - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ label: "MyLabel" }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); - - test("should return a node directive with additional labels", () => { - const typeDefs = ` - type TestType @node(additionalLabels:["Label", "AnotherLabel"]) { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ additionalLabels: ["Label", "AnotherLabel"] }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); - - test("should return a node directive with a label and additional labels", () => { - const typeDefs = ` - type TestType @node(label:"MyLabel", additionalLabels:["Label", "AnotherLabel"]) { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ label: "MyLabel", additionalLabels: ["Label", "AnotherLabel"] }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); - - test("should return a node directive with custom plural", () => { - const typeDefs = ` - type TestType @node(plural: "testTypes") { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ plural: "testTypes" }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); -}); diff --git a/packages/graphql/src/schema/parse-node-directive.ts b/packages/graphql/src/schema/parse-node-directive.ts index 9b9e0edf63..deafe053c4 100644 --- a/packages/graphql/src/schema/parse-node-directive.ts +++ b/packages/graphql/src/schema/parse-node-directive.ts @@ -21,28 +21,12 @@ import type { DirectiveNode } from "graphql"; import { valueFromASTUntyped } from "graphql"; import { NodeDirective } from "../classes/NodeDirective"; -const labelDeprecationWarning = - "NOTE: The label and additionalLabels arguments have been deprecated and will be removed in version 4.0.0. " + - "Please use the labels argument instead. More information can be found at " + - "https://neo4j.com/docs/graphql-manual/current/guides/v4-migration/" + - "#_label_and_additionalLabels_arguments_removed_from_node_and_replaced_with_new_argument_labels"; -let labelDeprecationWarningShown = false; - function parseNodeDirective(nodeDirective: DirectiveNode | undefined) { if (!nodeDirective || nodeDirective.name.value !== "node") { throw new Error("Undefined or incorrect directive passed into parseNodeDirective function"); } - const label = getArgumentValue(nodeDirective, "label"); - const additionalLabels = getArgumentValue(nodeDirective, "additionalLabels"); - if ((label || additionalLabels) && !labelDeprecationWarningShown) { - console.warn(labelDeprecationWarning); - labelDeprecationWarningShown = true; - } - return new NodeDirective({ - label, - additionalLabels, labels: getArgumentValue(nodeDirective, "labels"), }); } diff --git a/packages/graphql/tests/e2e/deprecated/delete-relationship-via-delete-additional-labels.e2e.test.ts b/packages/graphql/tests/e2e/deprecated/delete-relationship-via-delete-additional-labels.e2e.test.ts deleted file mode 100644 index 268b70eb4a..0000000000 --- a/packages/graphql/tests/e2e/deprecated/delete-relationship-via-delete-additional-labels.e2e.test.ts +++ /dev/null @@ -1,1490 +0,0 @@ -/* - * 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 { Driver } from "neo4j-driver"; -import supertest from "supertest"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import type { TestGraphQLServer } from "../setup/apollo-server"; -import { ApolloTestServer } from "../setup/apollo-server"; -import { TestSubscriptionsPlugin } from "../../utils/TestSubscriptionPlugin"; -import { WebSocketTestClient } from "../setup/ws-client"; -import Neo4j from "../setup/neo4j"; -import { cleanNodes } from "../../utils/clean-nodes"; -import { delay } from "../../../src/utils/utils"; - -describe("Delete Subscriptions when only nodes are targeted - when nodes employ @node directive to configure db label and additionalLabels", () => { - let neo4j: Neo4j; - let driver: Driver; - let server: TestGraphQLServer; - let wsClient: WebSocketTestClient; - let wsClient2: WebSocketTestClient; - let typeMovie: UniqueType; - let typeActor: UniqueType; - let typePerson: UniqueType; - let typeDinosaur: UniqueType; - let typeFilm: UniqueType; - let typeSeries: UniqueType; - let typeProduction: UniqueType; - let typeDefs: string; - - beforeEach(async () => { - typeActor = new UniqueType("Actor"); - typePerson = new UniqueType("Person"); - typeDinosaur = new UniqueType("Dinosaur"); - typeMovie = new UniqueType("Movie"); - typeFilm = new UniqueType("Film"); - typeSeries = new UniqueType("Series"); - typeProduction = new UniqueType("Production"); - - typeDefs = ` - type ${typeActor} @node(additionalLabels: ["${typePerson}"]) { - name: String - movies: [${typeMovie}!]! @relationship(type: "ACTED_IN", direction: OUT) - productions: [${typeProduction}!]! @relationship(type: "PART_OF", direction: OUT) - } - - type ${typeDinosaur} @node(label: "${typePerson}") { - name: String - movies: [${typeMovie}!]! @relationship(type: "DIRECTED", direction: OUT) - } - - type ${typePerson} { - name: String - movies: [${typeMovie}!]! @relationship(type: "DIRECTED", direction: OUT) - } - - type ${typeMovie} @node(label: "${typeFilm}", additionalLabels: ["Multimedia"]) { - id: ID - title: String - actors: [${typeActor}!]! @relationship(type: "ACTED_IN", direction: IN) - directors: [${typePerson}!]! @relationship(type: "DIRECTED", direction: IN) - } - - type ${typeSeries} @node(additionalLabels: ["${typeProduction}"]) { - title: String - actors: [${typeActor}!]! @relationship(type: "PART_OF", direction: IN) - productions: [${typeProduction}!]! @relationship(type: "IS_A", direction: OUT) - } - - type ${typeProduction} @node(additionalLabels: ["${typeSeries}"]) { - title: String - actors: [${typeActor}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - config: { - driverConfig: { - database: neo4j.getIntegrationDatabaseName(), - }, - }, - plugins: { - subscriptions: new TestSubscriptionsPlugin(), - }, - }); - server = new ApolloTestServer(neoSchema); - await server.start(); - - wsClient = new WebSocketTestClient(server.wsPath); - wsClient2 = new WebSocketTestClient(server.wsPath); - }); - - afterEach(async () => { - await wsClient.close(); - await wsClient2.close(); - - const session = driver.session(); - await cleanNodes(session, [typeActor, typeMovie, typePerson, typeFilm, typeSeries, typeProduction]); - - await server.close(); - await driver.close(); - }); - - const actorSubscriptionQuery = (typeActor) => ` - subscription SubscriptionActor { - ${typeActor.operations.subscribe.relationship_deleted} { - relationshipFieldName - event - ${typeActor.operations.subscribe.payload.relationship_deleted} { - name - } - deletedRelationship { - movies { - node { - title - } - } - productions { - node { - title - } - } - } - } - } - `; - - const movieSubscriptionQuery = (typeMovie) => ` - subscription SubscriptionMovie { - ${typeMovie.operations.subscribe.relationship_deleted} { - relationshipFieldName - event - ${typeMovie.operations.subscribe.payload.relationship_deleted} { - title - } - deletedRelationship { - actors { - node { - name - } - } - directors { - node { - name - } - } - } - } - } - `; - - const personSubscriptionQuery = (typePerson) => ` - subscription SubscriptionPerson { - ${typePerson.operations.subscribe.relationship_deleted} { - relationshipFieldName - event - ${typePerson.operations.subscribe.payload.relationship_deleted} { - name - } - deletedRelationship { - movies { - node { - title - } - } - } - } - } - `; - - const seriesSubscriptionQuery = (typeSeries) => ` - subscription SubscriptionSeries { - ${typeSeries.operations.subscribe.relationship_deleted} { - relationshipFieldName - event - ${typeSeries.operations.subscribe.payload.relationship_deleted} { - title - } - deletedRelationship { - actors { - node { - name - } - } - productions { - node { - title - } - } - } - } - } - `; - - test("disconnect via delete - find by label returns correct type when label specified", async () => { - // 1. create - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeActor.operations.create}( - input: [ - { - movies: { - create: [ - { - node: { - title: "John Wick" - } - }, - { - node: { - title: "Constantine" - } - } - ] - }, - name: "Keanu Reeves", - }, - { - name: "Keanu Reeves", - } - ] - ) { - ${typeActor.plural} { - name - } - } - } - `, - }) - .expect(200); - - // 2. subscribe both ways - await wsClient2.subscribe(movieSubscriptionQuery(typeMovie)); - - await wsClient.subscribe(actorSubscriptionQuery(typeActor)); - - // 3. perform update on created node - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeActor.operations.delete}( - where: { - name: "Keanu Reeves" - } - ) { - nodesDeleted - relationshipsDeleted - } - } - `, - }) - .expect(200); - - expect(wsClient.errors).toEqual([]); - expect(wsClient2.errors).toEqual([]); - - expect(wsClient2.events).toHaveLength(2); - expect(wsClient.events).toHaveLength(2); - - expect(wsClient2.events).toIncludeSameMembers([ - { - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "John Wick" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Keanu Reeves", - }, - }, - directors: null, - }, - }, - }, - { - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Keanu Reeves", - }, - }, - directors: null, - }, - }, - }, - ]); - expect(wsClient.events).toIncludeSameMembers([ - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "John Wick", - }, - }, - productions: null, - }, - }, - }, - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine", - }, - }, - productions: null, - }, - }, - }, - ]); - }); - - test("disconnect via delete - find by label returns correct type when additionalLabels specified", async () => { - // 1. create - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeMovie.operations.create}( - input: [ - { - actors: { - create: [ - { - node: { - name: "Someone" - } - }, - { - node: { - name: "Someone else" - } - } - ] - }, - title: "Constantine 3", - }, - { - actors: { - create: [ - { - node: { - name: "Someone" - } - } - ] - }, - title: "Constantine 2", - } - ] - ) { - ${typeMovie.plural} { - title - } - } - } - `, - }) - .expect(200); - - // 2. subscribe both ways - await wsClient2.subscribe(movieSubscriptionQuery(typeMovie)); - - await wsClient.subscribe(actorSubscriptionQuery(typeActor)); - - // 3. perform update on created node - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeMovie.operations.delete}( - where: { - title_STARTS_WITH: "Constantine" - } - ) { - nodesDeleted - relationshipsDeleted - } - } - `, - }) - .expect(200); - - expect(wsClient.errors).toEqual([]); - expect(wsClient2.errors).toEqual([]); - - expect(wsClient2.events).toHaveLength(3); - expect(wsClient.events).toHaveLength(3); - - expect(wsClient2.events).toIncludeSameMembers([ - { - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Someone", - }, - }, - directors: null, - }, - }, - }, - { - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Someone else", - }, - }, - directors: null, - }, - }, - }, - { - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 2" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Someone", - }, - }, - directors: null, - }, - }, - }, - ]); - expect(wsClient.events).toIncludeSameMembers([ - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Someone", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 3", - }, - }, - productions: null, - }, - }, - }, - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Someone else", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 3", - }, - }, - productions: null, - }, - }, - }, - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Someone", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 2", - }, - }, - productions: null, - }, - }, - }, - ]); - }); - - test("disconnect via delete - relation to two types of same underlying db type", async () => { - // 1. create - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typePerson.operations.create}( - input: [ - { - name: "Person someone", - } - ] - ) { - ${typePerson.plural} { - name - } - } - } - `, - }) - .expect(200); - - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeDinosaur.operations.create}( - input: [ - { - name: "Dinosaur someone", - } - ] - ) { - ${typeDinosaur.plural} { - name - } - } - } - `, - }) - .expect(200); - - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeMovie.operations.create}( - input: [ - { - directors: { - connect: [ - { - where: { - node: { - name: "Person someone" - } - } - }, - { - where: { - node: { - name: "Dinosaur someone" - } - } - } - ] - }, - title: "Constantine 3", - }, - { - directors: { - create: [ - { - node: { - name: "Dinosaur or Person" - } - } - ] - }, - title: "Constantine 2", - } - ] - ) { - ${typeMovie.plural} { - title - } - } - } - `, - }) - .expect(200); - - // 2. subscribe both ways - await wsClient2.subscribe(movieSubscriptionQuery(typeMovie)); - - await wsClient.subscribe(personSubscriptionQuery(typePerson)); - - // 3. perform update on created node - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeMovie.operations.delete}( - where: { - title_STARTS_WITH: "Constantine" - } - ) { - nodesDeleted - relationshipsDeleted - } - } - `, - }) - .expect(200); - - expect(wsClient.errors).toEqual([]); - expect(wsClient2.errors).toEqual([]); - - expect(wsClient2.events).toHaveLength(6); - expect(wsClient.events).toHaveLength(3); - - expect(wsClient2.events).toIncludeSameMembers([ - { - // 1. movie + person - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Person someone", - }, - }, - actors: null, - }, - }, - }, - { - // 2. movie + dinosaur - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Person someone", - }, - }, - actors: null, - }, - }, - }, - { - // 1. movie + person - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Dinosaur someone", - }, - }, - actors: null, - }, - }, - }, - { - // 2. movie + dinosaur - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 3" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Dinosaur someone", - }, - }, - actors: null, - }, - }, - }, - { - // 1. movie + person - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 2" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Dinosaur or Person", - }, - }, - actors: null, - }, - }, - }, - { - // 2. movie + dinosaur - [typeMovie.operations.subscribe.relationship_deleted]: { - [typeMovie.operations.subscribe.payload.relationship_deleted]: { title: "Constantine 2" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "directors", - deletedRelationship: { - directors: { - node: { - name: "Dinosaur or Person", - }, - }, - actors: null, - }, - }, - }, - ]); - expect(wsClient.events).toIncludeSameMembers([ - { - [typePerson.operations.subscribe.relationship_deleted]: { - [typePerson.operations.subscribe.payload.relationship_deleted]: { - name: "Person someone", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 3", - }, - }, - }, - }, - }, - { - [typePerson.operations.subscribe.relationship_deleted]: { - [typePerson.operations.subscribe.payload.relationship_deleted]: { - name: "Dinosaur someone", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 3", - }, - }, - }, - }, - }, - { - [typePerson.operations.subscribe.relationship_deleted]: { - [typePerson.operations.subscribe.payload.relationship_deleted]: { - name: "Dinosaur or Person", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "movies", - deletedRelationship: { - movies: { - node: { - title: "Constantine 2", - }, - }, - }, - }, - }, - ]); - }); - - test("disconnect via delete - relation to two types of matching with same labels", async () => { - // 1. create - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeProduction.operations.create}( - input: [ - { - title: "A Production", - } - ] - ) { - ${typeProduction.plural} { - title - } - } - } - `, - }) - .expect(200); - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeSeries.operations.create}( - input: [ - { - title: "A Series", - } - ] - ) { - ${typeSeries.plural} { - title - } - } - } - `, - }) - .expect(200); - - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeActor.operations.create}( - input: [ - { - productions: { - connect: [ - { - where: { - node: { - title: "A Production" - } - } - }, - { - where: { - node: { - title: "A Series" - } - } - } - ], - create: [ - { - node: { - title: "Constantine" - } - } - ] - }, - name: "Keanu Reeves", - }, - { - name: "Keanu Reeves", - } - ] - ) { - ${typeActor.plural} { - name - } - } - } - `, - }) - .expect(200); - - // 2. subscribe both ways - await wsClient2.subscribe(actorSubscriptionQuery(typeActor)); - - await wsClient.subscribe(seriesSubscriptionQuery(typeSeries)); - - // 3. perform update on created node - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeActor.operations.delete}( - where: { - name: "Keanu Reeves" - } - ) { - nodesDeleted - relationshipsDeleted - } - } - `, - }) - .expect(200); - - await delay(2); - expect(wsClient.errors).toEqual([]); - expect(wsClient2.errors).toEqual([]); - expect(wsClient2.events).toHaveLength(6); - expect(wsClient.events).toHaveLength(3); - - expect(wsClient2.events).toIncludeSameMembers([ - { - // 1. actor + series - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Constantine", - }, - }, - movies: null, - }, - }, - }, - { - // 2. actor + production - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Constantine", - }, - }, - movies: null, - }, - }, - }, - { - // 1. actor + series - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Series", - }, - }, - movies: null, - }, - }, - }, - { - // 2. actor + production - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Series", - }, - }, - movies: null, - }, - }, - }, - { - // 1. actor + series - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Production", - }, - }, - movies: null, - }, - }, - }, - { - // 2. actor + production - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Production", - }, - }, - movies: null, - }, - }, - }, - ]); - expect(wsClient.events).toIncludeSameMembers([ - { - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "A Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Keanu Reeves", - }, - }, - productions: null, - }, - }, - }, - { - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Constantine" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Keanu Reeves", - }, - }, - productions: null, - }, - }, - }, - { - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "A Production" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - actors: { - node: { - name: "Keanu Reeves", - }, - }, - productions: null, - }, - }, - }, - ]); - }); - - test("disconnect via delete - relation to self via other type with same labels", async () => { - // 1. create - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeProduction.operations.create}( - input: [ - { - title: "A Production", - } - ] - ) { - ${typeProduction.plural} { - title - } - } - } - `, - }) - .expect(200); - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeSeries.operations.create}( - input: [ - { - title: "A Series", - } - ] - ) { - ${typeSeries.plural} { - title - } - } - } - `, - }) - .expect(200); - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeSeries.operations.create}( - input: [ - { - title: "Another Series", - productions: { - connect: [ - { - where: { - node: { - title: "A Production" - } - } - }, - { - where: { - node: { - title: "A Series" - } - } - } - ], - create: [ - { - node: { - title: "Another Production" - } - } - ] - } - } - ] - ) { - ${typeSeries.plural} { - title - } - } - } - `, - }) - .expect(200); - - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeActor.operations.create}( - input: [ - { - productions: { - connect: [ - { - where: { - node: { - title: "Another Series" - } - } - } - ], - create: [ - { - node: { - title: "Constantine" - } - } - ] - }, - name: "Keanu Reeves", - }, - ] - ) { - ${typeActor.plural} { - name - } - } - } - `, - }) - .expect(200); - - // 2. subscribe both ways - await wsClient2.subscribe(actorSubscriptionQuery(typeActor)); - - await wsClient.subscribe(seriesSubscriptionQuery(typeSeries)); - - // 3. perform update on created node - await supertest(server.path) - .post("") - .send({ - query: ` - mutation { - ${typeSeries.operations.delete}( - where: { - title: "Another Series" - } - ) { - nodesDeleted - relationshipsDeleted - } - } - `, - }) - .expect(200); - - await delay(3); - expect(wsClient.errors).toEqual([]); - expect(wsClient2.errors).toEqual([]); - - expect(wsClient2.events).toHaveLength(2); - expect(wsClient.events).toHaveLength(10); - - expect(wsClient2.events).toIncludeSameMembers([ - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Series", - }, - }, - movies: null, - }, - }, - }, - { - [typeActor.operations.subscribe.relationship_deleted]: { - [typeActor.operations.subscribe.payload.relationship_deleted]: { - name: "Keanu Reeves", - }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Series", - }, - }, - movies: null, - }, - }, - }, - ]); - - expect(wsClient.events).toIncludeSameMembers([ - { - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "actors", - deletedRelationship: { - productions: null, - actors: { - node: { - name: "Keanu Reeves", - }, - }, - }, - }, - }, - { - // 1. series + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Series", - }, - }, - actors: null, - }, - }, - }, - { - // 2. series + production - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Series", - }, - }, - actors: null, - }, - }, - }, - { - // 3. production + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "A Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Series", - }, - }, - actors: null, - }, - }, - }, - { - // 1. series + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Production", - }, - }, - actors: null, - }, - }, - }, - { - // 2. series + production - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "A Production", - }, - }, - actors: null, - }, - }, - }, - { - // 3. production + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "A Production" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Series", - }, - }, - actors: null, - }, - }, - }, - { - // 1. series + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Production", - }, - }, - actors: null, - }, - }, - }, - { - // 2. series + production - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Series" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Production", - }, - }, - actors: null, - }, - }, - }, - { - // 3. production + series - [typeSeries.operations.subscribe.relationship_deleted]: { - [typeSeries.operations.subscribe.payload.relationship_deleted]: { title: "Another Production" }, - event: "DELETE_RELATIONSHIP", - - relationshipFieldName: "productions", - deletedRelationship: { - productions: { - node: { - title: "Another Series", - }, - }, - actors: null, - }, - }, - }, - ]); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-1049.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-1049.int.test.ts deleted file mode 100644 index f89ba9a0f0..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-1049.int.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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, Session } from "neo4j-driver"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; - -describe("https://github.com/neo4j/graphql/issues/1049", () => { - let schema: GraphQLSchema; - let neo4j: Neo4j; - let driver: Driver; - let session: Session; - - const Book = new UniqueType("Book"); - const Film = new UniqueType("Film"); - const Person = new UniqueType("Person"); - const Media = new UniqueType("Media"); - - async function graphqlQuery(query: string) { - return graphql({ - schema, - source: query, - contextValue: neo4j.getContextValues(), - }); - } - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - - const typeDefs = ` - interface ${Media.name} { - id: ID! @id - title: String! - likedBy: [${Person.name}!]! @relationship(type: "LIKES", direction: IN) - similar: [${Media.name}!]! - @cypher( - statement: """ - MATCH (this)<-[:LIKES]-(:${Person.name})-[:LIKES]->(other:${Media.name}) - RETURN COLLECT(other { .*, __resolveType: apoc.coll.subtract(labels(other), ['Meda'])[0] }) - """ - ) - } - - type ${Book.name} implements ${Media.name} @node(additionalLabels: ["${Media.name}"]) { - id: ID! - title: String! - likedBy: [${Person.name}!]! - similar: [${Media.name}!]! - - pageCount: Int! - } - - type ${Film.name} implements ${Media.name} @node(additionalLabels: ["${Media.name}"]) { - id: ID! - title: String! - likedBy: [${Person.name}!]! - similar: [${Media.name}!]! - - runTime: Int! - } - - type ${Person.name} { - name: String - likes: [${Media.name}!]! @relationship(type: "LIKES", direction: OUT) - } - `; - - session = await neo4j.getSession(); - - const neoGraphql = new Neo4jGraphQL({ typeDefs, driver }); - schema = await neoGraphql.getSchema(); - }); - - afterAll(async () => { - await session.close(); - await driver.close(); - }); - - test("error should not be thrown", async () => { - const mutation = ` - mutation { - ${Person.operations.create}( - input: [ - { - name: "Bob" - likes: { - create: [ - { node: { ${Book.name}: { title: "Harry Potter", pageCount: 300 } } } - { node: { ${Book.name}: { title: "Lord of the Rings", pageCount: 400 } } } - ] - } - } - ] - ) { - info { - nodesCreated - } - } - } - `; - - const mutationResult = await graphqlQuery(mutation); - expect(mutationResult.errors).toBeUndefined(); - - const query = ` - query { - ${Person.plural}(where: { likesConnection_SOME: { node: { title: "Harry Potter" } } }) { - name - } - } - `; - - const queryResult = await graphqlQuery(query); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ [Person.plural]: [{ name: "Bob" }] }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-1249.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-1249.int.test.ts deleted file mode 100644 index 9cdf098608..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-1249.int.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; - -describe("https://github.com/neo4j/graphql/issues/1249", () => { - let schema: GraphQLSchema; - let driver: Driver; - let neo4j: Neo4j; - - const typeDefs = ` - type Bulk - @exclude(operations: [CREATE, DELETE, UPDATE]) - @node(additionalLabels: ["$context.cypherParams.tenant"]) { - id: ID! - supplierMaterialNumber: String! - material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) - } - - type Material @exclude(operations: [CREATE, DELETE, UPDATE]) { - id: ID! - itemNumber: String! - - suppliers: [Supplier!]! - @relationship(type: "MATERIAL_SUPPLIER", properties: "RelationMaterialSupplier", direction: OUT) - } - - type Supplier @exclude(operations: [CREATE, DELETE, UPDATE]) { - id: ID! - name: String - supplierId: String! - } - - interface RelationMaterialSupplier @relationshipProperties { - supplierMaterialNumber: String! - } - `; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should pass the cypherParams from the context correctly at the top level translate", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - bulks { - supplierMaterialNumber - material { - id - suppliersConnection { - edges { - supplierMaterialNumber - node { - supplierId - } - } - } - } - } - } - `; - - const res = await graphql({ - schema, - source: query, - contextValue: neo4j.getContextValues({ cypherParams: { tenant: "BULK" } }), - }); - - expect(res.errors).toBeUndefined(); - expect(res.data).toEqual({ - bulks: [], - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-1848.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-1848.int.test.ts deleted file mode 100644 index 5c95c82465..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-1848.int.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; - -describe("https://github.com/neo4j/graphql/issues/1848", () => { - let schema: GraphQLSchema; - let driver: Driver; - let neo4j: Neo4j; - - const typeDefs = ` - type ContentPiece @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - } - - type Project @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - } - - type Community @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - hasContentPieces: [ContentPiece!]! - @relationship(type: "COMMUNITY_CONTENTPIECE_HASCONTENTPIECES", direction: OUT) - hasAssociatedProjects: [Project!]! - @relationship(type: "COMMUNITY_PROJECT_HASASSOCIATEDPROJECTS", direction: OUT) - } - - extend type Community { - """ - Used on Community Landing Page - """ - hasFeedItems(limit: Int = 10, pageIndex: Int = 0): [FeedItem!]! - @cypher( - statement: """ - Match(this)-[:COMMUNITY_CONTENTPIECE_HASCONTENTPIECES|:COMMUNITY_PROJECT_HASASSOCIATEDPROJECTS]-(pag) return pag SKIP ($limit * $pageIndex) LIMIT $limit - """ - ) - } - - union FeedItem = ContentPiece | Project - `; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should resolve union in cypher directive correctly", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - communities { - id - hasFeedItems { - ... on ContentPiece { - id - } - ... on Project { - id - } - } - } - } - `; - - const res = await graphql({ - schema, - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(res.errors).toBeUndefined(); - - expect(res.data).toEqual({ - communities: [], - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-2614.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-2614.int.test.ts deleted file mode 100644 index c049bd85c2..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-2614.int.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 { Driver, Session } from "neo4j-driver"; -import { graphql } from "graphql"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import { cleanNodes } from "../../utils/clean-nodes"; - -describe("https://github.com/neo4j/graphql/issues/2614", () => { - let driver: Driver; - let neo4j: Neo4j; - let neoSchema: Neo4jGraphQL; - let session: Session; - - let Actor: UniqueType; - let Movie: UniqueType; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - beforeEach(async () => { - session = await neo4j.getSession(); - - Actor = new UniqueType("Actor"); - Movie = new UniqueType("Movie"); - - const typeDefs = ` - interface Production { - title: String! - actors: [${Actor}!]! - } - - type ${Movie} implements Production @node(label:"Film"){ - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! - } - - type Series implements Production { - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! - } - - interface ActedIn @relationshipProperties { - role: String! - } - - type ${Actor} { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - `; - - await session.run(` - CREATE (a:${Actor} { name: "Jack Adams" })-[:ACTED_IN]->(m1:Film { title: "The Movie we want", runtime: 123 }) - CREATE (a)-[:ACTED_IN]->(m2:${Movie} { title: "The Movie we do not want", runtime: 234 }) - `); - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - }); - - afterEach(async () => { - await cleanNodes(session, [Actor, Movie]); - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should use the provided node directive label in the where clause", async () => { - const query = ` - query GetProductionsMovie { - ${Actor.plural} { - name - actedIn(where: { _on: { ${Movie.name}: {} } }) { - title - } - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Actor.plural]: [ - { - name: "Jack Adams", - actedIn: [ - { - title: "The Movie we want", - }, - ], - }, - ], - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-2709.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-2709.int.test.ts deleted file mode 100644 index fa8b53894f..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-2709.int.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -/* - * 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 { Driver, Session } from "neo4j-driver"; -import { graphql } from "graphql"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import { cleanNodes } from "../../utils/clean-nodes"; - -describe("https://github.com/neo4j/graphql/issues/2709", () => { - let driver: Driver; - let neo4j: Neo4j; - let neoSchema: Neo4jGraphQL; - let session: Session; - - let Movie: UniqueType; - let Dishney: UniqueType; - let Netflix: UniqueType; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - beforeEach(async () => { - session = await neo4j.getSession(); - - Movie = new UniqueType("Movie"); - Dishney = new UniqueType("Dishney"); - Netflix = new UniqueType("Netflix"); - - const typeDefs = ` - interface Production { - title: String! - actors: [Actor!]! - distribution: [DistributionHouse!]! - } - - type ${Movie} implements Production @node(label: "Film") { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - type Series implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - interface ActedIn @relationshipProperties { - role: String! - } - - interface Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - type MaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - rating: Int! - } - type FemaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - age: Int! - } - - interface DistributionHouse { - name: String! - } - - type ${Dishney} implements DistributionHouse { - name: String! - review: String! - } - - type Prime implements DistributionHouse { - name: String! - review: String! - } - - type ${Netflix} implements DistributionHouse { - name: String! - review: String! - } - `; - - await session.run(` - CREATE (:Film { title: "A Netflix movie" })<-[:DISTRIBUTED_BY]-(:${Netflix} { name: "Netflix" }) - CREATE (:Film { title: "A Dishney movie" })<-[:DISTRIBUTED_BY]-(:${Dishney} { name: "Dishney" }) - `); - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - }); - - afterEach(async () => { - await cleanNodes(session, [Movie, Netflix, Dishney]); - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should query only DistributionHouses with the label Netflix", async () => { - const query = ` - query { - ${Movie.plural}( - where: { OR: [{ distributionConnection_SOME: { node: { _on: { ${Netflix}: {} }, name: "Netflix" } } }] } - ) { - title - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: [ - { - title: "A Netflix movie", - }, - ], - }); - }); - - test("should query only DistributionHouses with the label Dishney", async () => { - const query = ` - query { - ${Movie.plural}( - where: { OR: [{ distributionConnection_SOME: { node: { _on: { ${Dishney}: {} }, name: "Dishney" } } }] } - ) { - title - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: [ - { - title: "A Dishney movie", - }, - ], - }); - }); - - test("should query the correct DistributionHouses when no _on present - Netflix", async () => { - const query = ` - query { - ${Movie.plural}( - where: { distributionConnection_SOME: { node: { name: "Netflix" } } } - ) { - title - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: [ - { - title: "A Netflix movie", - }, - ], - }); - }); - - test("should query the correct DistributionHouses when no _on present - Dishney", async () => { - const query = ` - query { - ${Movie.plural}( - where: { distributionConnection_SOME: { node: { name: "Dishney" } } } - ) { - title - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: [ - { - title: "A Dishney movie", - }, - ], - }); - }); -}); - -describe("https://github.com/neo4j/graphql/issues/2709 - extended", () => { - let driver: Driver; - let neo4j: Neo4j; - let neoSchema: Neo4jGraphQL; - let session: Session; - - let Movie: UniqueType; - let Dishney: UniqueType; - let Netflix: UniqueType; - let Publisher: UniqueType; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - beforeEach(async () => { - session = await neo4j.getSession(); - - Movie = new UniqueType("Movie"); - Dishney = new UniqueType("Dishney"); - Netflix = new UniqueType("Netflix"); - Publisher = new UniqueType("Publisher"); - - const typeDefs = ` - interface Production { - title: String! - actors: [Actor!]! - distribution: [DistributionHouse!]! - } - - type ${Movie} implements Production @node(label: "Film") { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - publisher: ${Publisher}! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - type Series implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - interface ActedIn @relationshipProperties { - role: String! - } - - interface Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - type MaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - rating: Int! - } - type FemaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - age: Int! - } - - interface DistributionHouse { - name: String! - } - - type ${Dishney} implements DistributionHouse { - name: String! - review: String! - } - - type Prime implements DistributionHouse { - name: String! - review: String! - } - - type ${Netflix} implements DistributionHouse { - name: String! - review: String! - } - - ### Extension ### - type ${Publisher} { - name: String! - } - ################ - `; - - await session.run(` - CREATE (:Film { title: "A Netflix movie" })<-[:DISTRIBUTED_BY]-(:${Netflix} { name: "Netflix" }) - CREATE (:Film { title: "A Dishney movie" })<-[:DISTRIBUTED_BY]-(:${Dishney} { name: "Dishney" }) - CREATE (:Film { title: "A Publisher movie" })<-[:DISTRIBUTED_BY]-(:${Publisher} { name: "The Publisher" }) - `); - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - }); - - afterEach(async () => { - await cleanNodes(session, [Movie, Netflix, Dishney, Publisher]); - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should query only DistributionHouse nodes and NOT nodes (Publisher) which are connected by the same rel-type (DISTRIBUTED_BY)", async () => { - const query = ` - query { - ${Movie.plural}( - where: { distributionConnection_SOME: { node: { name_CONTAINS: "e" } } } - ) { - title - } - } - `; - - const result = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValues(), - }); - - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: expect.toIncludeSameMembers([ - { - title: "A Netflix movie", - }, - { - title: "A Dishney movie", - }, - ]), - }); - // Note: to not equal - expect(result.data as any).not.toEqual({ - [Movie.plural]: expect.toIncludeSameMembers([ - { - title: "A Netflix movie", - }, - { - title: "A Dishney movie", - }, - { - title: "A Publisher movie", - }, - ]), - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-976.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-976.int.test.ts deleted file mode 100644 index af4ebb1b8d..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-976.int.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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 { DocumentNode, GraphQLSchema } from "graphql"; -import { graphql } from "graphql"; -import type { Driver, Integer, Session } from "neo4j-driver"; -import { gql } from "apollo-server"; -import Neo4j from "../neo4j"; -import { getQuerySource } from "../../utils/get-query-source"; -import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; - -describe("https://github.com/neo4j/graphql/issues/976", () => { - const testBibliographicReference = new UniqueType("BibliographicReference"); - const testConcept = new UniqueType("Concept"); - let schema: GraphQLSchema; - let driver: Driver; - let neo4j: Neo4j; - let session: Session; - - async function graphqlQuery(query: DocumentNode) { - return graphql({ - schema, - source: getQuerySource(query), - contextValue: neo4j.getContextValues(), - }); - } - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - - const typeDefs = ` - type ${testBibliographicReference.name} @node(additionalLabels: ["Resource"]){ - iri: ID! @unique @alias(property: "uri") - prefLabel: [String!] - isInPublication: [${testConcept.name}!]! @relationship(type: "isInPublication", direction: OUT) - } - - type ${testConcept.name} @node(additionalLabels: ["Resource"]){ - iri: ID! @unique @alias(property: "uri") - prefLabel: [String!]! - } - `; - const neoGraphql = new Neo4jGraphQL({ typeDefs, driver }); - schema = await neoGraphql.getSchema(); - }); - - beforeEach(async () => { - session = await neo4j.getSession(); - }); - - afterEach(async () => { - await session.run(`MATCH (bibRef:${testBibliographicReference.name}) DETACH DELETE bibRef`); - await session.run(`MATCH (concept:${testConcept.name}) DETACH DELETE concept`); - - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should query nested connection", async () => { - const createBibRefQuery = gql` - mutation { - ${testBibliographicReference.operations.create}( - input: { - iri: "urn:myiri2" - prefLabel: "Initial label" - isInPublication: { - create: { node: { iri: "new-e", prefLabel: "stuff" } } - } - } - ) { - ${testBibliographicReference.plural} { - iri - prefLabel - isInPublication { - iri - prefLabel - } - } - } - } - `; - const updateBibRefQuery = gql` - mutation { - ${testConcept.operations.delete}(where: { iri: "new-e" }) { - nodesDeleted - } - - ${testBibliographicReference.operations.update}( - where: { iri: "urn:myiri2" } - update: { - prefLabel: "Updated Label" - isInPublication: [ - { - connectOrCreate: { - where: { node: { iri: "new-g" } } - onCreate: { node: { iri: "new-g", prefLabel: "pub" } } - } - } - { - connectOrCreate: { - where: { node: { iri: "new-f" } } - onCreate: { node: { iri: "new-f", prefLabel: "pub" } } - } - } - ] - } - ) { - ${testBibliographicReference.plural} { - iri - prefLabel - isInPublication(where: { iri_IN: ["new-f", "new-e"] }) { - iri - prefLabel - } - } - } - } - `; - const createBibRefResult = await graphqlQuery(createBibRefQuery); - expect(createBibRefResult.errors).toBeUndefined(); - - const bibRefRes = await session.run(` - MATCH (bibRef:${testBibliographicReference.name})-[r:isInPublication]->(concept:${testConcept.name}) RETURN bibRef.uri as bibRefUri, concept.uri as conceptUri - `); - - expect(bibRefRes.records).toHaveLength(1); - expect(bibRefRes.records[0].toObject().bibRefUri as string).toBe("urn:myiri2"); - expect(bibRefRes.records[0].toObject().conceptUri as string).toBe("new-e"); - - const updateBibRefResult = await graphqlQuery(updateBibRefQuery); - expect(updateBibRefResult.errors).toBeUndefined(); - expect(updateBibRefResult?.data).toEqual({ - [testConcept.operations.delete]: { - nodesDeleted: 1, - }, - [testBibliographicReference.operations.update]: { - [testBibliographicReference.plural]: [ - { - iri: "urn:myiri2", - prefLabel: ["Updated Label"], - isInPublication: [ - { - iri: "new-f", - prefLabel: ["pub"], - }, - ], - }, - ], - }, - }); - - const conceptCount = await session.run(` - MATCH (bibRef:${testBibliographicReference.name})-[r:isInPublication]->(concept:${testConcept.name}) RETURN bibRef.uri as bibRefUri, COUNT(concept) as conceptCount - `); - - expect(conceptCount.records).toHaveLength(1); - expect(conceptCount.records[0].toObject().bibRefUri as string).toBe("urn:myiri2"); - expect((conceptCount.records[0].toObject().conceptCount as Integer).toNumber()).toBe(2); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-escape-in-union.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-escape-in-union.int.test.ts deleted file mode 100644 index d30b58dc1b..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-escape-in-union.int.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import { graphql } from "graphql"; -import type { Driver, Session } from "neo4j-driver"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; - -describe("Empty fields on unions due to escaped labels", () => { - let driver: Driver; - let neo4j: Neo4j; - let session: Session; - let neoSchema: Neo4jGraphQL; - - const typeBlog = new UniqueType("Blog"); - const typePost = new UniqueType("Post"); - const typeUser = new UniqueType("User"); - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - const typeDefs = gql` - union Content = Blog | Post - - type Blog @node(label: "${typeBlog.name}") { - title: String - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT) - } - - type Post @node(label: "${typePost.name}") { - content: String - } - - type User @node(label: "${typeUser.name}") { - name: String - content: [Content!]! @relationship(type: "HAS_CONTENT", direction: OUT) - } - `; - - neoSchema = new Neo4jGraphQL({ typeDefs }); - session = await neo4j.getSession(); - await session.run(`CREATE (u:${typeUser.name} {name: "dan"}) - CREATE (b:${typeBlog.name} {title:"my cool blog"}) - CREATE (p:${typePost.name} {content: "my cool post"}) - - MERGE(u)-[:HAS_CONTENT]->(b) - MERGE(b)-[:HAS_POST]->(p) - `); - }); - - afterAll(async () => { - await session.close(); - await driver.close(); - }); - - test("should return users and unions", async () => { - const query = ` - query GetUsersWithAllContent { - users { - name - content(where: { Blog: { title_NOT: null } }) { - ... on Blog { - title - } - } - } - } - `; - - const gqlResult: any = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), - }); - expect(gqlResult.errors).toBeUndefined(); - expect(gqlResult.data).toEqual({ - users: [{ name: "dan", content: [{ title: "my cool blog" }] }], - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-fulltext.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-fulltext.int.test.ts deleted file mode 100644 index daf9753211..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-fulltext.int.test.ts +++ /dev/null @@ -1,2776 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { Driver, Session } from "neo4j-driver"; -import { graphql, GraphQLSchema } from "graphql"; -import { generate } from "randomstring"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import { upperFirst } from "../../../src/utils/upper-first"; -import { delay } from "../../../src/utils/utils"; -import { isMultiDbUnsupportedError } from "../../utils/is-multi-db-unsupported-error"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { SCORE_FIELD } from "../../../src/graphql/directives/fulltext"; - -function generatedTypeDefs(personType: UniqueType, movieType: UniqueType): string { - return ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; -} - -describe("@fulltext directive", () => { - let driver: Driver; - let neo4j: Neo4j; - let databaseName: string; - let MULTIDB_SUPPORT = true; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - - databaseName = generate({ readable: true, charset: "alphabetic" }); - - const cypher = `CREATE DATABASE ${databaseName} WAIT`; - const session = driver.session(); - - try { - await session.run(cypher); - } catch (e) { - if (e instanceof Error) { - if (isMultiDbUnsupportedError(e)) { - // No multi-db support, so we skip tests - MULTIDB_SUPPORT = false; - } else { - throw e; - } - } - } finally { - await session.close(); - } - - await delay(5000); - }); - - afterAll(async () => { - if (MULTIDB_SUPPORT) { - const cypher = `DROP DATABASE ${databaseName}`; - - const session = await neo4j.getSession(); - try { - await session.run(cypher); - } finally { - await session.close(); - } - } - - await driver.close(); - }); - - describe("Query Tests", () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - let session: Session; - let neoSchema: Neo4jGraphQL; - let generatedSchema: GraphQLSchema; - let personType: UniqueType; - let movieType: UniqueType; - let personTypeLowerFirst: string; - let queryType: string; - - const person1 = { - name: "this is a name", - born: 1984, - }; - const person2 = { - name: "This is a different name", - born: 1985, - }; - const person3 = { - name: "Another name", - born: 1986, - }; - const movie1 = { - title: "Some Title", - description: "some other description", - released: 2001, - }; - const movie2 = { - title: "Another Title", - description: "this is a description", - released: 2002, - }; - - beforeEach(async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - personType = new UniqueType("Person"); - movieType = new UniqueType("Movie"); - queryType = `${personType.plural}Fulltext${upperFirst(personType.name)}Index`; - personTypeLowerFirst = personType.singular; - - const typeDefs = generatedTypeDefs(personType, movieType); - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - session = driver.session({ database: databaseName }); - - try { - await session.run( - ` - CREATE (person1:${personType.name})-[:ACTED_IN]->(movie1:${movieType.name}) - CREATE (person1)-[:ACTED_IN]->(movie2:${movieType.name}) - CREATE (person2:${personType.name})-[:ACTED_IN]->(movie1) - CREATE (person3:${personType.name})-[:ACTED_IN]->(movie2) - SET person1 = $person1 - SET person2 = $person2 - SET person3 = $person3 - SET movie1 = $movie1 - SET movie2 = $movie2 - `, - { person1, person2, person3, movie1, movie2 } - ); - } finally { - await session.close(); - } - }); - - test("Orders by score DESC as default", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual({ - name: person2.name, - }); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual({ - name: person1.name, - }); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst]).toEqual({ - name: person3.name, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD] - ); - }); - - test("Order updates when using a different phrase", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "some name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual({ - name: person3.name, - }); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual({ - name: person1.name, - }); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst]).toEqual({ - name: person2.name, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD] - ); - }); - - test("No results if phrase doesn't match", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "should not match") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([]); - }); - - test("Filters node to single result", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { name: "${person1.name}" } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Filters node to multiple results", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { born_GTE: ${person2.born} } }) { - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: { - name: person2.name, - }, - }, - { - [personTypeLowerFirst]: { - name: person3.name, - }, - }, - ]); - }); - - test("Filters node to no results", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { name_CONTAINS: "not in anything!!" } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([]); - }); - - test("Filters score to single result", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { min: 0.5 } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person2.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Filters score to multiple results", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { max: 0.5 } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst].name).toBe(person3.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(2); - }); - - test("Filters score to no results", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { min: 100 } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([]); - }); - - test("Filters score with combined min and max", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { min: 0.201, max: 0.57 } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Filters score with max score of 0", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { max: 0 } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([]); - }); - - test("Throws error if score filtered with a non-number", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const nonNumberScoreInput = "not a number"; - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { max: "${nonNumberScoreInput}" } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toBe( - `Float cannot represent non numeric value: "${nonNumberScoreInput}"` - ); - }); - - test("Filters a related node to multiple values", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { actedInMovies_SOME: { title: "${movie1.title}" } } }) { - score - ${personTypeLowerFirst} { - name - actedInMovies(options: { sort: [{ released: DESC }] }) { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person2.name); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].actedInMovies).toEqual([ - { - title: movie1.title, - released: movie1.released, - }, - ]); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst].actedInMovies).toEqual([ - { - title: movie2.title, - released: movie2.released, - }, - { - title: movie1.title, - released: movie1.released, - }, - ]); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(2); - }); - - test("Filters a related node to a single value", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { actedInMovies_ALL: { released: ${movie1.released} } } }) { - ${personTypeLowerFirst} { - name - actedInMovies { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: { - name: person2.name, - actedInMovies: [ - { - title: movie1.title, - released: movie1.released, - }, - ], - }, - }, - ]); - }); - - test("Filters a related node to no values", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { actedInMovies_ALL: { released_NOT_IN: [${movie1.released}, ${movie2.released}] } } }) { - score - ${personTypeLowerFirst} { - name - actedInMovies { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([]); - }); - - test("Throws an error for a non-string phrase", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const nonStringValue = '["not", "a", "string"]'; - const query = ` - query { - ${queryType}(phrase: ${nonStringValue}) { - score - ${personTypeLowerFirst} { - name - actedInMovies { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toBe( - `String cannot represent a non string value: ${nonStringValue}` - ); - }); - - test("Throws an error for an invalid where", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const invalidField = "not_a_field"; - const query = ` - query { - ${queryType}(phrase: "some name", where: { ${personTypeLowerFirst}: { ${invalidField}: "invalid" } }) { - score - ${personTypeLowerFirst} { - name - actedInMovies { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toStartWith( - `Field "${invalidField}" is not defined by type` - ); - }); - - test("Sorting by score ascending", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", sort: { score: ASC }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person3.name); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst].name).toBe(person2.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeLessThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeLessThanOrEqual( - (gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD] - ); - }); - - test("Sorting by node", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", sort: [{ ${personTypeLowerFirst}: { name: ASC } }]) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person3.name); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst].name).toBe(person2.name); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst].name).toBe(person1.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeNumber(); - expect((gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD]).toBeNumber(); - }); - - test("Unordered sorting", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "this is", sort: { ${personTypeLowerFirst}: { born: ASC, name: DESC } }) { - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: person1, - }, - { - [personTypeLowerFirst]: person2, - }, - ]); - }); - - test("Ordered sorting, no score", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const person1 = { - name: "a b c", - born: 123, - }; - const person2 = { - name: "b c d", - born: 234, - }; - - session = driver.session({ database: databaseName }); - - try { - await session.run( - ` - CREATE (person1:${personType.name}) - CREATE (person2:${personType.name}) - SET person1 = $person1 - SET person2 = $person2 - `, - { person1, person2 } - ); - } finally { - await session.close(); - } - - const query1 = ` - query { - ${queryType}(phrase: "b", sort: [{ ${personTypeLowerFirst}: { born: DESC } }, { ${personTypeLowerFirst}: { name: ASC } }]) { - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const query2 = ` - query { - ${queryType}(phrase: "b", sort: [{ ${personTypeLowerFirst}: { name: ASC } }, { ${personTypeLowerFirst}: { born: DESC } }]) { - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult1 = await graphql({ - schema: generatedSchema, - source: query1, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - const gqlResult2 = await graphql({ - schema: generatedSchema, - source: query2, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult1.errors).toBeFalsy(); - expect(gqlResult2.errors).toBeFalsy(); - expect(gqlResult1.data?.[queryType]).toEqual([ - { [personTypeLowerFirst]: person2 }, - { [personTypeLowerFirst]: person1 }, - ]); - expect(gqlResult2.data?.[queryType]).toEqual([ - { [personTypeLowerFirst]: person1 }, - { [personTypeLowerFirst]: person2 }, - ]); - }); - - test("Ordered sorting, with score", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const person1 = { - name: "a b c", - born: 123, - }; - const person2 = { - name: "b c d", - born: 234, - }; - - session = driver.session({ database: databaseName }); - - try { - await session.run( - ` - CREATE (person1:${personType.name}) - CREATE (person2:${personType.name}) - SET person1 = $person1 - SET person2 = $person2 - `, - { person1, person2 } - ); - } finally { - await session.close(); - } - - const query1 = ` - query { - ${queryType}(phrase: "b d", sort: [{ score: DESC }, { ${personTypeLowerFirst}: { name: ASC } }]) { - score - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const query2 = ` - query { - ${queryType}(phrase: "b d", sort: [{ ${personTypeLowerFirst}: { name: ASC } }, { score: DESC }]) { - score - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult1 = await graphql({ - schema: generatedSchema, - source: query1, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - const gqlResult2 = await graphql({ - schema: generatedSchema, - source: query2, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult1.errors).toBeFalsy(); - expect(gqlResult2.errors).toBeFalsy(); - expect((gqlResult1.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual(person2); - expect((gqlResult1.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect((gqlResult1.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual(person1); - expect((gqlResult1.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeNumber(); - expect((gqlResult2.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual(person1); - expect((gqlResult2.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect((gqlResult2.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual(person2); - expect((gqlResult2.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeNumber(); - }); - - test("Sort on nested field", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a name") { - ${personTypeLowerFirst} { - name - actedInMovies(options: { sort: [{ released: ASC }] }) { - title - released - } - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: { - name: person1.name, - actedInMovies: [ - { - title: movie1.title, - released: movie1.released, - }, - { - title: movie2.title, - released: movie2.released, - }, - ], - }, - }, - { - [personTypeLowerFirst]: { - name: person2.name, - actedInMovies: [ - { - title: movie1.title, - released: movie1.released, - }, - ], - }, - }, - { - [personTypeLowerFirst]: { - name: person3.name, - actedInMovies: [ - { - title: movie2.title, - released: movie2.released, - }, - ], - }, - }, - ]); - }); - - test("Combined filter and sort", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a name", sort: { score: ASC }, where: { score: { min: 0.2 } }) { - score - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual(person2); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual(person1); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeLessThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(2); - }); - - test("Limiting is possible", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a name", limit: 2) { - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toBeArrayOfSize(2); - }); - - test("Offsetting is possible", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a name", offset: 2) { - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: person3, - }, - ]); - }); - - test("Combined limiting and offsetting is possible", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a name", limit: 1, offset: 1) { - score - ${personTypeLowerFirst} { - name - born - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual(person2); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Sorting by score when the score is not returned", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", sort: { score: ASC }) { - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: { - name: person3.name, - }, - }, - { - [personTypeLowerFirst]: { - name: person1.name, - }, - }, - { - [personTypeLowerFirst]: { - name: person2.name, - }, - }, - ]); - }); - - test("Sort by node when node is not returned", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "this is", sort: { ${personTypeLowerFirst}: { born: ASC } }) { - score - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeNumber(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toBeUndefined(); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toBeUndefined(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(2); - }); - - test("Filters by node when node is not returned", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { ${personTypeLowerFirst}: { name: "${person1.name}" } }) { - score - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toBeUndefined(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Filters by score when no score is returned", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const query = ` - query { - ${queryType}(phrase: "a different name", where: { score: { max: 0.5 } }) { - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType]).toEqual([ - { - [personTypeLowerFirst]: { - name: person1.name, - }, - }, - { - [personTypeLowerFirst]: { - name: person3.name, - }, - }, - ]); - }); - - test("Works with @auth 'where' when authenticated", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ where: { name: "$jwt.name" } }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { name: person1.name }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual({ - name: person1.name, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Works with @auth 'where' when unauthenticated", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ where: { name: "$jwt.name" } }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { name: "Not a name" }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(0); - }); - - test("Works with @auth 'roles' when authenticated", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ roles: ["admin"] }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { roles: ["admin"] }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual({ - name: person1.name, - }); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual({ - name: person2.name, - }); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst]).toEqual({ - name: person3.name, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD] - ); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(3); - }); - - test("Works with @auth 'roles' when unauthenticated", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ roles: ["admin"] }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { roles: ["not_admin"] }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); - }); - - test("Works with @auth 'allow' when all match", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ allow: { name: "$jwt.name" } }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name", where: { ${personTypeLowerFirst}: { name: "${person2.name}" } }) { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { name: person2.name }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst].name).toBe(person2.name); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeNumber(); - expect(gqlResult.data?.[queryType] as any[]).toBeArrayOfSize(1); - }); - - test("Works with @auth 'allow' when one match", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ allow: { name: "$jwt.name" } }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { name: person2.name }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); - }); - - test("Works with @auth 'roles' when only READ operation is specified", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ indexName: "${personType.name}Index", fields: ["name"] }]) - @auth(rules: [{ roles: ["admin"], operations: [READ] }]) { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} { - title: String! - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - const secret = "This is a secret"; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - - const req = createJwtRequest(secret, { roles: ["not_admin"] }); - - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - req, - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); - }); - - test("Multiple fulltext index fields", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const moveTypeLowerFirst = movieType.singular; - queryType = `${movieType.plural}Fulltext${upperFirst(movieType.name)}Index`; - const typeDefs = ` - type ${personType.name} { - name: String! - born: Int! - actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${movieType.name} @fulltext(indexes: [{ indexName: "${movieType.name}Index", fields: ["title", "description"] }]) { - title: String! - description: String - released: Int! - actors: [${personType.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "some description") { - score - ${moveTypeLowerFirst} { - title - description - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][moveTypeLowerFirst]).toEqual({ - title: movie1.title, - description: movie1.description, - }); - expect((gqlResult.data?.[queryType] as any[])[1][moveTypeLowerFirst]).toEqual({ - title: movie2.title, - description: movie2.description, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - }); - - test("Custom query name", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - personType = new UniqueType("Person"); - personTypeLowerFirst = personType.singular; - queryType = "CustomQueryName"; - - session = driver.session({ database: databaseName }); - - try { - await session.run( - ` - CREATE (person1:${personType.name}) - CREATE (person2:${personType.name}) - CREATE (person3:${personType.name}) - SET person1 = $person1 - SET person2 = $person2 - SET person3 = $person3 - `, - { person1, person2, person3 } - ); - } finally { - await session.close(); - } - - const typeDefs = ` - type ${personType.name} @fulltext(indexes: [{ queryName: "${queryType}", indexName: "${personType.name}CustomIndex", fields: ["name"] }]) { - name: String! - born: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "a different name") { - score - ${personTypeLowerFirst} { - name - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][personTypeLowerFirst]).toEqual({ - name: person2.name, - }); - expect((gqlResult.data?.[queryType] as any[])[1][personTypeLowerFirst]).toEqual({ - name: person1.name, - }); - expect((gqlResult.data?.[queryType] as any[])[2][personTypeLowerFirst]).toEqual({ - name: person3.name, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - expect((gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[2][SCORE_FIELD] - ); - }); - - test("Multiple index fields with custom query name", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const moveTypeLowerFirst = movieType.singular; - queryType = "SomeCustomQueryName"; - const typeDefs = ` - type ${movieType.name} @fulltext(indexes: [{ queryName: "${queryType}", indexName: "${movieType.name}Index", fields: ["title", "description"] }]) { - title: String! - description: String - released: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query = ` - query { - ${queryType}(phrase: "some description") { - score - ${moveTypeLowerFirst} { - title - description - } - } - } - `; - const gqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[queryType] as any[])[0][moveTypeLowerFirst]).toEqual({ - title: movie1.title, - description: movie1.description, - }); - expect((gqlResult.data?.[queryType] as any[])[1][moveTypeLowerFirst]).toEqual({ - title: movie2.title, - description: movie2.description, - }); - expect((gqlResult.data?.[queryType] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult.data?.[queryType] as any[])[1][SCORE_FIELD] - ); - }); - - test("Creating and querying multiple indexes", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - movieType = new UniqueType("Movie"); - const movieTypeLowerFirst = movieType.singular; - const queryType1 = "CustomQueryName"; - const queryType2 = "CustomQueryName2"; - - session = driver.session({ database: databaseName }); - - try { - await session.run( - ` - CREATE (movie1:${movieType.name}) - CREATE (movie2:${movieType.name}) - SET movie1 = $movie1 - SET movie2 = $movie2 - `, - { movie1, movie2 } - ); - } finally { - await session.close(); - } - - const typeDefs = ` - type ${movieType.name} @fulltext(indexes: [ - { queryName: "${queryType1}", indexName: "${movieType.name}CustomIndex", fields: ["title"] }, - { queryName: "${queryType2}", indexName: "${movieType.name}CustomIndex2", fields: ["description"] } - ]) { - title: String! - description: String! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, - }); - generatedSchema = await neoSchema.getSchema(); - await neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }); - - const query1 = ` - query { - ${queryType1}(phrase: "some title") { - score - ${movieTypeLowerFirst} { - title - } - } - } - `; - const query2 = ` - query { - ${queryType2}(phrase: "some description") { - score - ${movieTypeLowerFirst} { - title - } - } - } - `; - const gqlResult1 = await graphql({ - schema: generatedSchema, - source: query1, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - const gqlResult2 = await graphql({ - schema: generatedSchema, - source: query2, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(gqlResult1.errors).toBeFalsy(); - expect((gqlResult1.data?.[queryType1] as any[])[0][movieTypeLowerFirst]).toEqual({ - title: movie1.title, - }); - expect((gqlResult1.data?.[queryType1] as any[])[1][movieTypeLowerFirst]).toEqual({ - title: movie2.title, - }); - expect((gqlResult1.data?.[queryType1] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult1.data?.[queryType1] as any[])[1][SCORE_FIELD] - ); - - expect(gqlResult2.errors).toBeFalsy(); - expect((gqlResult2.data?.[queryType2] as any[])[0][movieTypeLowerFirst]).toEqual({ - title: movie1.title, - }); - expect((gqlResult2.data?.[queryType2] as any[])[1][movieTypeLowerFirst]).toEqual({ - title: movie2.title, - }); - expect((gqlResult2.data?.[queryType2] as any[])[0][SCORE_FIELD]).toBeGreaterThanOrEqual( - (gqlResult2.data?.[queryType2] as any[])[1][SCORE_FIELD] - ); - }); - }); - describe("Index Creation", () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - let type: UniqueType; - - const indexName1 = "indexCreationName1"; - const indexName2 = "indexCreationName2"; - const label = "someCustomLabel"; - const aliasName = "someFieldAlias"; - - const indexQueryCypher = ` - SHOW INDEXES yield - name AS name, - type AS type, - entityType AS entityType, - labelsOrTypes AS labelsOrTypes, - properties AS properties - WHERE name = "${indexName1}" OR name = "${indexName2}" - RETURN { - name: name, - type: type, - entityType: entityType, - labelsOrTypes: labelsOrTypes, - properties: properties - } as result - ORDER BY result.name ASC - `; - - const deleteIndex1Cypher = ` - DROP INDEX ${indexName1} IF EXISTS - `; - const deleteIndex2Cypher = ` - DROP INDEX ${indexName2} IF EXISTS - `; - - beforeEach(() => { - type = new UniqueType("Movie"); - }); - - afterEach(async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const session = driver.session({ database: databaseName }); - - try { - await session.run(deleteIndex1Cypher); - await session.run(deleteIndex2Cypher); - } finally { - await session.close(); - } - }); - - test("Creates index if it doesn't exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) { - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - try { - const result = await session.run(indexQueryCypher); - - expect(result.records[0].get("result")).toEqual({ - name: indexName1, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [type.name], - properties: ["title"], - }); - } finally { - await session.close(); - } - }); - - test("Creates two index's if they dont exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }, { indexName: "${indexName2}", fields: ["description"] }]) { - title: String! - description: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - try { - const result = await session.run(indexQueryCypher); - - expect(result.records[0].get("result")).toEqual({ - name: indexName1, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [type.name], - properties: ["title"], - }); - expect(result.records[1].get("result")).toEqual({ - name: indexName2, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [type.name], - properties: ["description"], - }); - } finally { - await session.close(); - } - }); - - test("When using the node label, creates index if it doesn't exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) @node(label: "${label}") { - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - try { - const result = await session.run(indexQueryCypher); - - expect(result.records[0].get("result")).toEqual({ - name: indexName1, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [label], - properties: ["title"], - }); - } finally { - await session.close(); - } - }); - - test("When using the field alias, creates index if it doesn't exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) @node(label: "${label}") { - title: String! @alias(property: "${aliasName}") - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - try { - const result = await session.run(indexQueryCypher); - - expect(result.records[0].get("result")).toEqual({ - name: indexName1, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [label], - properties: [aliasName], - }); - } finally { - await session.close(); - } - }); - - test("Throws when missing index (create index and constraint option not true)", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) { - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).rejects.toThrow(`Missing @fulltext index '${indexName1}' on Node '${type.name}'`); - }); - - test("Throws when an index is missing fields", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title", "description"] }]) { - title: String! - description: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - try { - await session.run( - [ - `CREATE FULLTEXT INDEX ${indexName1}`, - `IF NOT EXISTS FOR (n:${type.name})`, - `ON EACH [n.title]`, - ].join(" ") - ); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).rejects.toThrow(`@fulltext index '${indexName1}' on Node '${type.name}' is missing field 'description'`); - }); - - test("When using the field alias, throws when index is missing fields", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title", "description"] }]) { - title: String! - description: String! @alias(property: "${aliasName}") - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - try { - await session.run( - [ - `CREATE FULLTEXT INDEX ${indexName1}`, - `IF NOT EXISTS FOR (n:${type.name})`, - `ON EACH [n.title, n.description]`, - ].join(" ") - ); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).rejects.toThrow( - `@fulltext index '${indexName1}' on Node '${type.name}' is missing field 'description' aliased to field '${aliasName}'` - ); - }); - - test("Doesn't throw if an index exists", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) { - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - }); - - test("Throws when index is missing fields when used with create option", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title", "description"] }]) { - title: String! - description: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - try { - await session.run( - [ - `CREATE FULLTEXT INDEX ${indexName1}`, - `IF NOT EXISTS FOR (n:${type.name})`, - `ON EACH [n.title]`, - ].join(" ") - ); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).rejects.toThrow( - `@fulltext index '${indexName1}' on Node '${type.name}' already exists, but is missing field 'description'` - ); - }); - - test("Create index for ID field if it doesn't exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const typeDefs = gql` - type ${type.name} @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["id"] }]) { - id: ID! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - try { - const result = await session.run(indexQueryCypher); - - expect(result.records[0].get("result")).toEqual({ - name: indexName1, - type: "FULLTEXT", - entityType: "NODE", - labelsOrTypes: [type.name], - properties: ["id"], - }); - } finally { - await session.close(); - } - }); - - test("should not throw if index exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) { - title: String! - } - `; - - const createIndexCypher = ` - CREATE FULLTEXT INDEX ${indexName1} - IF NOT EXISTS FOR (n:${additionalType.name}) - ON EACH [n.title] - `; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createIndexCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - } finally { - await session.close(); - } - }); - - test("should not create new constraint if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) @fulltext(indexes: [{ indexName: "${indexName1}", fields: ["title"] }]) { - title: String! - } - `; - - const createIndexCypher = ` - CREATE FULLTEXT INDEX ${indexName1} - IF NOT EXISTS FOR (n:${additionalType.name}) - ON EACH [n.title] - `; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createIndexCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const dbConstraintsResult = (await session.run(indexQueryCypher)).records.map((record) => { - return record.toObject().result; - }); - - expect( - dbConstraintsResult.filter( - (record) => record.labelsOrTypes.includes(baseType.name) && record.properties.includes("title") - ) - ).toHaveLength(0); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(additionalType.name) && record.properties.includes("title") - ) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-label-cypher-injection.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-label-cypher-injection.int.test.ts deleted file mode 100644 index 7752d983ee..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-label-cypher-injection.int.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import type { Driver, Session } from "neo4j-driver"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; -import { createJwtRequest } from "../../utils/create-jwt-request"; - -describe("Label cypher injection", () => { - let schema: GraphQLSchema; - let driver: Driver; - let neo4j: Neo4j; - let session: Session; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - }); - - beforeEach(async () => { - session = await neo4j.getSession({ defaultAccessMode: "WRITE" }); - }); - - afterEach(async () => { - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("should escape the label name passed in context", async () => { - const typeMovie = new UniqueType("Movie"); - - const typeDefs = ` - type ${typeMovie} @node(label: "$context.label") { - title: String - } - `; - - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - ${typeMovie.plural} { - title - } - } - `; - - const res = await graphql({ - schema, - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark(), { - label: "Movie\\u0060) MATCH", - }), - }); - - expect(res.errors).toBeUndefined(); - }); - - test("should escape the label name passed through jwt", async () => { - const typeMovie = new UniqueType("Movie"); - - const typeDefs = ` - type ${typeMovie} @node(label: "$jwt.label") { - title: String - } - `; - - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "1234", - }), - }, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - ${typeMovie.plural} { - title - } - } - `; - - const req = createJwtRequest("1234", { label: "Movie\\u0060) MATCH" }); - - const res = await graphql({ - schema, - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark(), { req }), - }); - - expect(res.errors).toBeUndefined(); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-label.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-label.int.test.ts deleted file mode 100644 index 2c5e132b78..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-label.int.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import type { Driver, Session } from "neo4j-driver"; -import { graphql } from "graphql"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import { createJwtRequest } from "../../utils/create-jwt-request"; - -describe("Node directive labels", () => { - let driver: Driver; - let neo4j: Neo4j; - let session: Session; - - const typeFilm = new UniqueType("Film"); - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - session = await neo4j.getSession(); - - await session.run(`CREATE (m:${typeFilm.name} {title: "The Matrix",year:1999})`); - }); - - afterAll(async () => { - await driver.close(); - await session.close(); - }); - - test("custom labels", async () => { - const typeDefs = `type Movie @node(label: "${typeFilm.name}") { - id: ID - title: String - }`; - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - - const query = `query { - movies { - title - } - }`; - - const gqlResult = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), - }); - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult as any).data.movies[0]).toEqual({ - title: "The Matrix", - }); - }); - - test("custom jwt labels", async () => { - const typeDefs = `type Movie @node(label: "$jwt.filmLabel") { - id: ID - title: String - }`; - - const secret = "1234"; - - const req = createJwtRequest(secret, { filmLabel: typeFilm.name }); - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - - const query = `query { - movies { - title - } - }`; - - const gqlResult = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark(), { req }), - }); - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult as any).data.movies[0]).toEqual({ - title: "The Matrix", - }); - }); - - test("custom context labels", async () => { - const typeDefs = `type Movie @node(label: "$context.filmLabel") { - id: ID - title: String - }`; - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - - const query = `query { - movies { - title - } - }`; - - const gqlResult = await graphql({ - schema: await neoSchema.getSchema(), - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark(), { - filmLabel: typeFilm.name, - }), - }); - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult as any).data.movies[0]).toEqual({ - title: "The Matrix", - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-limit.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-limit.int.test.ts deleted file mode 100644 index f10b784c43..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-limit.int.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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, Session } from "neo4j-driver"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; - -describe("https://github.com/neo4j/graphql/issues/1628", () => { - const workType = new UniqueType("Work"); - const titleType = new UniqueType("Title"); - - let schema: GraphQLSchema; - let neo4j: Neo4j; - let driver: Driver; - let session: Session; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - - const typeDefs = ` - type ${workType} @node(additionalLabels: ["Resource"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - """ - IRI - """ - iri: ID! @unique @alias(property: "uri") - title: [${titleType}!]! @relationship(type: "title", direction: OUT) - } - - type ${titleType} @node(additionalLabels: ["property"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - value: String - } - `; - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - }); - schema = await neoGraphql.getSchema(); - }); - - beforeEach(async () => { - session = await neo4j.getSession(); - }); - - afterEach(async () => { - await session.close(); - }); - - afterAll(async () => { - await driver.close(); - }); - - test("Nested filter with limit cypher should be composed correctly", async () => { - const query = ` - { - ${workType.plural}(options: { limit: 1 }, where: { title: { value_CONTAINS: "0777" } }) { - title(where: { value_CONTAINS: "0777" }) { - value - } - } - } - `; - - await session.run(` - CREATE (t:${workType}:Resource)-[:title]->(:${titleType}:property {value: "bond0777"}) - CREATE (t)-[:title]->(:${titleType}:property {value: "bond0777"}) - `); - - const result = await graphql({ - schema, - source: query, - contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), - }); - expect(result.errors).toBeUndefined(); - expect(result.data as any).toEqual({ - [workType.plural]: [ - { - title: [ - { - value: "bond0777", - }, - { - value: "bond0777", - }, - ], - }, - ], - }); - }); -}); diff --git a/packages/graphql/tests/integration/deprecated/labels-unique.int.test.ts b/packages/graphql/tests/integration/deprecated/labels-unique.int.test.ts deleted file mode 100644 index 3bd6b1e84c..0000000000 --- a/packages/graphql/tests/integration/deprecated/labels-unique.int.test.ts +++ /dev/null @@ -1,1162 +0,0 @@ -/* - * 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 { Driver } from "neo4j-driver"; -import { generate } from "randomstring"; -import { graphql } from "graphql"; -import { gql } from "apollo-server"; -import Neo4j from "../neo4j"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import { isMultiDbUnsupportedError } from "../../utils/is-multi-db-unsupported-error"; -import { getNeo4jDatabaseInfo, Neo4jDatabaseInfo } from "../../../src/classes/Neo4jDatabaseInfo"; -import { Executor } from "../../../src/classes/Executor"; - -describe("assertIndexesAndConstraints/unique", () => { - let driver: Driver; - let neo4j: Neo4j; - let databaseName: string; - let MULTIDB_SUPPORT = true; - let dbInfo: Neo4jDatabaseInfo; - - beforeAll(async () => { - neo4j = new Neo4j(); - driver = await neo4j.getDriver(); - dbInfo = await getNeo4jDatabaseInfo(new Executor({ executionContext: driver })); - - databaseName = generate({ readable: true, charset: "alphabetic" }); - - const cypher = `CREATE DATABASE ${databaseName} WAIT`; - const session = driver.session(); - - try { - await session.run(cypher); - } catch (e) { - if (e instanceof Error) { - if (isMultiDbUnsupportedError(e)) { - // No multi-db support, so we skip tests - MULTIDB_SUPPORT = false; - } else { - throw e; - } - } - } finally { - await session.close(); - } - - await new Promise((x) => setTimeout(x, 5000)); - }); - - afterAll(async () => { - if (MULTIDB_SUPPORT) { - const cypher = `DROP DATABASE ${databaseName}`; - - const session = await neo4j.getSession(); - try { - await session.run(cypher); - } finally { - await session.close(); - } - } - await driver.close(); - }); - - test("should create a constraint if it doesn't exist and specified in options, and then throw an error in the event of constraint validation", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const isbn = generate({ readable: true }); - const title = generate({ readable: true }); - - const typeDefs = gql` - type Book { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - const schema = await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes("Book")) - ).toHaveLength(1); - } finally { - await session.close(); - } - - const mutation = ` - mutation CreateBooks($isbn: String!, $title: String!) { - createBooks(input: [{ isbn: $isbn, title: $title }]) { - books { - isbn - title - } - } - } - `; - - const createResult = await graphql({ - schema, - source: mutation, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - variableValues: { - isbn, - title, - }, - }); - - expect(createResult.errors).toBeFalsy(); - - expect(createResult.data).toEqual({ - createBooks: { books: [{ isbn, title }] }, - }); - - const errorResult = await graphql({ - schema, - source: mutation, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - variableValues: { - isbn, - title, - }, - }); - - expect(errorResult.errors).toHaveLength(1); - expect(errorResult.errors?.[0].message).toBe("Constraint validation failed"); - }); - - describe("@unique", () => { - test("should throw an error when all necessary constraints do not exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.isbn`); - }); - - test("should throw an error when all necessary constraints do not exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.internationalStandardBookNumber`); - }); - - test("should not throw an error when all necessary constraints exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - const cypher = `CREATE CONSTRAINT ${type.name}_isbn ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.isbn IS UNIQUE`; - - try { - await session.run(cypher); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when all necessary constraints exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - const cypher = `CREATE CONSTRAINT ${type.name}_isbn ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.internationalStandardBookNumber IS UNIQUE`; - - try { - await session.run(cypher); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should create a constraint if it doesn't exist and specified in options", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes(type.name)) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - - test("should create a constraint if it doesn't exist and specified in options when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter( - (record) => - record.labelsOrTypes.includes(type.name) && - record.properties.includes("internationalStandardBookNumber") - ) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - - test("should not throw if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) { - someIntProperty: Int! - title: String! @unique - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_title ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.title IS UNIQUE; - `; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - } finally { - await session.close(); - } - }); - - test("should not create new constraint if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) { - someIntProperty: Int! - title: String! @unique - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_title ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.title IS UNIQUE; - `; - - const showConstraintsCypher = "SHOW UNIQUE CONSTRAINTS"; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const dbConstraintsResult = (await session.run(showConstraintsCypher)).records.map((record) => { - return record.toObject(); - }); - - expect( - dbConstraintsResult.filter( - (record) => record.labelsOrTypes.includes(baseType.name) && record.properties.includes("title") - ) - ).toHaveLength(0); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(additionalType.name) && record.properties.includes("title") - ) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - - test("should not allow creating duplicate @unique properties when constraint is on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) { - someStringProperty: String! @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - const generatedSchema = await neoSchema.getSchema(); - - const mutation = ` - mutation { - ${baseType.operations.create}(input: [ - { - someStringProperty: "notUnique", - title: "someTitle" - }, - { - someStringProperty: "notUnique", - title: "someTitle2" - }, - ]) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const query = ` - query { - ${baseType.plural} { - someStringProperty - } - } - `; - - const mutationGqlResult = await graphql({ - schema: generatedSchema, - source: mutation, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - const queryGqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect((mutationGqlResult?.errors as any[])[0].message).toBe("Constraint validation failed"); - - expect(queryGqlResult.errors).toBeFalsy(); - expect(queryGqlResult.data?.[baseType.plural]).toBeArrayOfSize(0); - } finally { - await session.close(); - } - }); - - test("should not allow updating to duplicate @unique properties when constraint is on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) { - someStringProperty: String! @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - const generatedSchema = await neoSchema.getSchema(); - - const uniqueVal1 = "someVal1"; - const uniqueVal2 = "someUniqueVal2"; - - const createMutation = ` - mutation { - ${baseType.operations.create}(input: [ - { - someStringProperty: "${uniqueVal1}", - title: "someTitle" - }, - { - someStringProperty: "${uniqueVal2}", - title: "someTitle2" - }, - ]) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const updateMutation = ` - mutation { - ${baseType.operations.update}(update: { - someStringProperty: "notUnique" - }) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const query = ` - query { - ${baseType.plural} { - someStringProperty - } - } - `; - - const createGqlResult = await graphql({ - schema: generatedSchema, - source: createMutation, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - const updateGqlResult = await graphql({ - schema: generatedSchema, - source: updateMutation, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - const queryGqlResult = await graphql({ - schema: generatedSchema, - source: query, - contextValue: { - driver, - driverConfig: { database: databaseName }, - }, - }); - - expect(createGqlResult?.errors).toBeFalsy(); - expect((updateGqlResult?.errors as any[])[0].message).toBe("Constraint validation failed"); - - expect(queryGqlResult.errors).toBeFalsy(); - expect(queryGqlResult.data?.[baseType.plural]).toIncludeSameMembers([ - { - someStringProperty: uniqueVal1, - }, - { - someStringProperty: uniqueVal2, - }, - ]); - } finally { - await session.close(); - } - }); - }); - - describe("@id", () => { - test("should throw an error when all necessary constraints do not exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.id`); - }); - - test("should throw an error when all necessary constraints do not exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.identifier`); - }); - - test("should not throw an error when unique argument is set to false", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id(unique: false) - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when unique argument is set to false when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id(unique: false) @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when all necessary constraints exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - const cypher = `CREATE CONSTRAINT ${type.name}_id ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.id IS UNIQUE`; - - try { - await session.run(cypher); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when all necessary constraints exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const session = driver.session({ database: databaseName }); - - const cypher = `CREATE CONSTRAINT ${type.name}_id ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.identifier IS UNIQUE`; - - try { - await session.run(cypher); - } finally { - await session.close(); - } - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, driverConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should create a constraint if it doesn't exist and specified in options", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes(type.name)) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - - test("should create a constraint if it doesn't exist and specified in options when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter( - (record) => - record.labelsOrTypes.includes(type.name) && record.properties.includes("identifier") - ) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - - test("should not create a constraint if it doesn't exist and unique option is set to false", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id(unique: false) - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes(type.name)) - ).toHaveLength(0); - } finally { - await session.close(); - } - }); - - test("should not create a constraint if it doesn't exist and unique option is set to false when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = new UniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id(unique: false) @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const session = driver.session({ database: databaseName }); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - try { - const result = await session.run(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter( - (record) => - record.labelsOrTypes.includes(type.name) && record.properties.includes("identifier") - ) - ).toHaveLength(0); - } finally { - await session.close(); - } - }); - - test("should not throw if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - someIdProperty: ID! @id @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - } finally { - await session.close(); - } - }); - - test("should not create new constraint if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = new UniqueType("Base"); - const additionalType = new UniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(additionalLabels: ["${additionalType.name}"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - someIdProperty: ID! @id @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - const showConstraintsCypher = "SHOW UNIQUE CONSTRAINTS"; - - const session = driver.session({ database: databaseName }); - - try { - await session.run(createConstraintCypher); - - const neoSchema = new Neo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - driverConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const dbConstraintsResult = (await session.run(showConstraintsCypher)).records.map((record) => { - return record.toObject(); - }); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(baseType.name) && record.properties.includes("someAlias") - ) - ).toHaveLength(0); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(additionalType.name) && - record.properties.includes("someAlias") - ) - ).toHaveLength(1); - } finally { - await session.close(); - } - }); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-1131.test.ts b/packages/graphql/tests/tck/deprecated/labels-1131.test.ts deleted file mode 100644 index 7fb030ca5b..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-1131.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/1131", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type BibliographicReference @node(additionalLabels: ["Resource"]) { - iri: ID! @unique @alias(property: "uri") - prefLabel: [String] - isInPublication: [Concept!]! @relationship(type: "isInPublication", direction: OUT) - } - - type Concept @node(additionalLabels: ["Resource"]) { - iri: ID! @unique @alias(property: "uri") - prefLabel: [String]! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - }); - }); - - test("where with multiple filters and params", async () => { - const query = gql` - mutation { - updateBibliographicReferences( - where: { iri: "urn:myiri2" } - update: { - prefLabel: "Updated Label:My BRS with Resource" - isInPublication: [ - { - connectOrCreate: { - where: { node: { iri: "new-g" } } - onCreate: { node: { iri: "new-g", prefLabel: "pub" } } - } - } - { - connectOrCreate: { - where: { node: { iri: "new-f" } } - onCreate: { node: { iri: "new-f", prefLabel: "pub" } } - } - } - ] - } - ) { - bibliographicReferences { - iri - prefLabel - isInPublication(where: { iri_IN: ["new-f", "new-e"] }) { - iri - prefLabel - } - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`BibliographicReference\`:\`Resource\`) - WHERE this.uri = $param0 - SET this.prefLabel = $this_update_prefLabel - WITH this - CALL { - WITH this - MERGE (this_isInPublication0_connectOrCreate0:\`Concept\`:\`Resource\` { uri: $this_isInPublication0_connectOrCreate_param0 }) - ON CREATE SET - this_isInPublication0_connectOrCreate0.uri = $this_isInPublication0_connectOrCreate_param1, - this_isInPublication0_connectOrCreate0.prefLabel = $this_isInPublication0_connectOrCreate_param2 - MERGE (this)-[this_isInPublication0_connectOrCreate_this0:isInPublication]->(this_isInPublication0_connectOrCreate0) - RETURN COUNT(*) AS _ - } - WITH this - CALL { - WITH this - MERGE (this_isInPublication1_connectOrCreate0:\`Concept\`:\`Resource\` { uri: $this_isInPublication1_connectOrCreate_param0 }) - ON CREATE SET - this_isInPublication1_connectOrCreate0.uri = $this_isInPublication1_connectOrCreate_param1, - this_isInPublication1_connectOrCreate0.prefLabel = $this_isInPublication1_connectOrCreate_param2 - MERGE (this)-[this_isInPublication1_connectOrCreate_this0:isInPublication]->(this_isInPublication1_connectOrCreate0) - RETURN COUNT(*) AS _ - } - WITH * - CALL { - WITH this - MATCH (this)-[update_this0:isInPublication]->(this_isInPublication:\`Concept\`:\`Resource\`) - WHERE this_isInPublication.uri IN $update_param0 - WITH this_isInPublication { iri: this_isInPublication.uri, .prefLabel } AS this_isInPublication - RETURN collect(this_isInPublication) AS this_isInPublication - } - RETURN collect(DISTINCT this { iri: this.uri, .prefLabel, isInPublication: this_isInPublication }) AS data" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"update_param0\\": [ - \\"new-f\\", - \\"new-e\\" - ], - \\"param0\\": \\"urn:myiri2\\", - \\"this_update_prefLabel\\": [ - \\"Updated Label:My BRS with Resource\\" - ], - \\"this_isInPublication0_connectOrCreate_param0\\": \\"new-g\\", - \\"this_isInPublication0_connectOrCreate_param1\\": \\"new-g\\", - \\"this_isInPublication0_connectOrCreate_param2\\": [ - \\"pub\\" - ], - \\"this_isInPublication1_connectOrCreate_param0\\": \\"new-f\\", - \\"this_isInPublication1_connectOrCreate_param1\\": \\"new-f\\", - \\"this_isInPublication1_connectOrCreate_param2\\": [ - \\"pub\\" - ], - \\"resolvedCallbacks\\": {} - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-1249.test.ts b/packages/graphql/tests/tck/deprecated/labels-1249.test.ts deleted file mode 100644 index d4255a0da9..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-1249.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/1249", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Bulk - @exclude(operations: [CREATE, DELETE, UPDATE]) - @node(additionalLabels: ["$context.cypherParams.tenant"]) { - id: ID! - supplierMaterialNumber: String! - material: Material! @relationship(type: "MATERIAL_BULK", direction: OUT) - } - - type Material @exclude(operations: [CREATE, DELETE, UPDATE]) { - id: ID! - itemNumber: String! - - suppliers: [Supplier!]! - @relationship(type: "MATERIAL_SUPPLIER", properties: "RelationMaterialSupplier", direction: OUT) - } - - type Supplier @exclude(operations: [CREATE, DELETE, UPDATE]) { - id: ID! - name: String - supplierId: String! - } - - interface RelationMaterialSupplier @relationshipProperties { - supplierMaterialNumber: String! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("should contain the cypherParams that are passed via the context", async () => { - const query = gql` - query { - bulks { - supplierMaterialNumber - material { - id - suppliersConnection { - edges { - supplierMaterialNumber - node { - supplierId - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { contextValues: { cypherParams: { tenant: "BULK" } } }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Bulk\`:\`BULK\`) - CALL { - WITH this - MATCH (this)-[this0:MATERIAL_BULK]->(this_material:\`Material\`) - CALL { - WITH this_material - MATCH (this_material)-[this_material_connection_suppliersConnectionthis0:MATERIAL_SUPPLIER]->(this_material_Supplier:\`Supplier\`) - WITH { supplierMaterialNumber: this_material_connection_suppliersConnectionthis0.supplierMaterialNumber, node: { supplierId: this_material_Supplier.supplierId } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this_material_suppliersConnection - } - WITH this_material { .id, suppliersConnection: this_material_suppliersConnection } AS this_material - RETURN head(collect(this_material)) AS this_material - } - RETURN this { .supplierMaterialNumber, material: this_material } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"cypherParams\\": { - \\"tenant\\": \\"BULK\\" - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-1628.test.ts b/packages/graphql/tests/tck/deprecated/labels-1628.test.ts deleted file mode 100644 index b69629e263..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-1628.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/1628", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type frbr__Work @node(additionalLabels: ["Resource"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - """ - IRI - """ - iri: ID! @unique @alias(property: "uri") - dcterms__title: [dcterms_title!]! @relationship(type: "dcterms__title", direction: OUT) - } - - type dcterms_title @node(additionalLabels: ["property"]) @exclude(operations: [CREATE, UPDATE, DELETE]) { - value: String - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Filter generated by query doesn't utilise index", async () => { - const query = gql` - { - frbrWorks(options: { limit: 10000 }, where: { dcterms__title: { value_CONTAINS: "0777" } }) { - iri - dcterms__title(where: { value_CONTAINS: "0777" }) { - value - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`frbr__Work\`:\`Resource\`) - WHERE EXISTS { - MATCH (this)-[:dcterms__title]->(this0:\`dcterms_title\`:\`property\`) - WHERE this0.value CONTAINS $param0 - } - WITH * - LIMIT $param1 - CALL { - WITH this - MATCH (this)-[this1:dcterms__title]->(this_dcterms__title:\`dcterms_title\`:\`property\`) - WHERE this_dcterms__title.value CONTAINS $param2 - WITH this_dcterms__title { .value } AS this_dcterms__title - RETURN collect(this_dcterms__title) AS this_dcterms__title - } - RETURN this { iri: this.uri, dcterms__title: this_dcterms__title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"0777\\", - \\"param1\\": { - \\"low\\": 10000, - \\"high\\": 0 - }, - \\"param2\\": \\"0777\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-1848.test.ts b/packages/graphql/tests/tck/deprecated/labels-1848.test.ts deleted file mode 100644 index d22fa4993d..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-1848.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/1848", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type ContentPiece @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - } - - type Project @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - } - - type Community @node(additionalLabels: ["UNIVERSAL"]) { - uid: String! @unique - id: Int - hasContentPieces: [ContentPiece!]! - @relationship(type: "COMMUNITY_CONTENTPIECE_HASCONTENTPIECES", direction: OUT) - hasAssociatedProjects: [Project!]! - @relationship(type: "COMMUNITY_PROJECT_HASASSOCIATEDPROJECTS", direction: OUT) - } - - extend type Community { - """ - Used on Community Landing Page - """ - hasFeedItems(limit: Int = 10, pageIndex: Int = 0): [FeedItem!]! - @cypher( - statement: """ - Match(this)-[:COMMUNITY_CONTENTPIECE_HASCONTENTPIECES|:COMMUNITY_PROJECT_HASASSOCIATEDPROJECTS]-(pag) return pag SKIP ($limit * $pageIndex) LIMIT $limit - """ - ) - } - - union FeedItem = ContentPiece | Project - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("should not concatenate AND and variable name", async () => { - const query = gql` - query { - communities { - id - hasFeedItems { - ... on ContentPiece { - id - } - ... on Project { - id - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Community\`:\`UNIVERSAL\`) - CALL { - WITH this - UNWIND apoc.cypher.runFirstColumnMany(\\"Match(this)-[:COMMUNITY_CONTENTPIECE_HASCONTENTPIECES|:COMMUNITY_PROJECT_HASASSOCIATEDPROJECTS]-(pag) return pag SKIP ($limit * $pageIndex) LIMIT $limit\\", { limit: $param0, pageIndex: $param1, this: this, auth: $auth }) AS this_hasFeedItems - WITH * - WHERE ((this_hasFeedItems:\`ContentPiece\` AND this_hasFeedItems:\`UNIVERSAL\`) OR (this_hasFeedItems:\`Project\` AND this_hasFeedItems:\`UNIVERSAL\`)) - RETURN collect(CASE - WHEN (this_hasFeedItems:\`ContentPiece\` AND this_hasFeedItems:\`UNIVERSAL\`) THEN this_hasFeedItems { __resolveType: \\"ContentPiece\\", .id } - WHEN (this_hasFeedItems:\`Project\` AND this_hasFeedItems:\`UNIVERSAL\`) THEN this_hasFeedItems { __resolveType: \\"Project\\", .id } - END) AS this_hasFeedItems - } - RETURN this { .id, hasFeedItems: this_hasFeedItems } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 10, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"auth\\": { - \\"isAuthenticated\\": false, - \\"roles\\": [] - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-2614.test.ts b/packages/graphql/tests/tck/deprecated/labels-2614.test.ts deleted file mode 100644 index 0f61176e5a..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-2614.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/2614", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = ` - interface Production { - title: String! - actors: [Actor!]! - } - - type Movie implements Production @node(label:"Film"){ - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! - } - - type Series implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! - } - - interface ActedIn @relationshipProperties { - role: String! - } - - type Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("should use the provided node directive label in the call subquery", async () => { - const query = gql` - query GetProductionsMovie { - actors { - actedIn(where: { _on: { Movie: { title: "Test Movie" } } }) { - title - } - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Actor\`) - WITH * - CALL { - WITH * - CALL { - WITH this - MATCH (this)-[this0:ACTED_IN]->(this_Movie:\`Film\`) - WHERE this_Movie.title = $param0 - RETURN { __resolveType: \\"Movie\\", title: this_Movie.title } AS this_actedIn - } - RETURN collect(this_actedIn) AS this_actedIn - } - RETURN this { actedIn: this_actedIn } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Test Movie\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-2709.test.ts b/packages/graphql/tests/tck/deprecated/labels-2709.test.ts deleted file mode 100644 index a308bd3ef2..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-2709.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("https://github.com/neo4j/graphql/issues/2709", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = ` - interface Production { - title: String! - actors: [Actor!]! - distribution: [DistributionHouse!]! - } - - type Movie implements Production @node(label: "Film") { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - runtime: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - type Series implements Production { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - episodes: Int! - distribution: [DistributionHouse!]! @relationship(type: "DISTRIBUTED_BY", direction: IN) - } - - interface ActedIn @relationshipProperties { - role: String! - } - - interface Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - type MaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - rating: Int! - } - type FemaleActor implements Actor { - name: String! - actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - age: Int! - } - - interface DistributionHouse { - name: String! - } - - type Dishney implements DistributionHouse { - name: String! - review: String! - } - - type Prime implements DistributionHouse { - name: String! - review: String! - } - - type Netflix implements DistributionHouse { - name: String! - review: String! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("should use the correct node label for connection rel when defined in node _on - Netflix label", async () => { - const query = gql` - query { - movies( - where: { OR: [{ distributionConnection_SOME: { node: { _on: { Netflix: {} }, name: "test" } } }] } - ) { - title - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE EXISTS { - MATCH (this1:\`Netflix\`)-[this0:DISTRIBUTED_BY]->(this) - WHERE this1.name = $param0 - } - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test\\" - }" - `); - }); - - test("should use the correct node label for connection rel when defined in node _on - Dishney label", async () => { - const query = gql` - query { - movies( - where: { OR: [{ distributionConnection_SOME: { node: { _on: { Dishney: {} }, name: "test2" } } }] } - ) { - title - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE EXISTS { - MATCH (this1:\`Dishney\`)-[this0:DISTRIBUTED_BY]->(this) - WHERE this1.name = $param0 - } - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test2\\" - }" - `); - }); - - test("should use the correct node label for connection rel when defined in node _on - without OR operator", async () => { - const query = gql` - query { - movies(where: { distributionConnection_SOME: { node: { _on: { Dishney: {} }, name: "test3" } } }) { - title - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE EXISTS { - MATCH (this1:\`Dishney\`)-[this0:DISTRIBUTED_BY]->(this) - WHERE this1.name = $param0 - } - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test3\\" - }" - `); - }); - - test("should not use a node label so it covers all nodes implementing the interface for connection rel", async () => { - const query = gql` - query { - movies(where: { distributionConnection_SOME: { node: { name: "test4" } } }) { - title - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE EXISTS { - MATCH (this1)-[this0:DISTRIBUTED_BY]->(this) - WHERE (this1.name = $param0 AND (this1:\`Dishney\` OR this1:\`Prime\` OR this1:\`Netflix\`)) - } - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test4\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-field-level-aggregations-labels.test.ts b/packages/graphql/tests/tck/deprecated/labels-field-level-aggregations-labels.test.ts deleted file mode 100644 index 2eb6bd0348..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-field-level-aggregations-labels.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Field Level Aggregations Alias", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Movie @node(label: "Film") { - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - } - - type Actor @node(label: "Person") { - name: String - age: Int - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - interface ActedIn { - time: Int - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - }); - }); - - test("Aggregation with labels", async () => { - const query = gql` - query { - movies { - actorsAggregate { - node { - name { - shortest - } - } - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - CALL { - WITH this - MATCH (this_actorsAggregate_this0:\`Person\`)-[this_actorsAggregate_this1:ACTED_IN]->(this) - WITH this_actorsAggregate_this0 - ORDER BY size(this_actorsAggregate_this0.name) DESC - WITH collect(this_actorsAggregate_this0.name) AS list - RETURN { longest: head(list), shortest: last(list) } AS this_actorsAggregate_var2 - } - RETURN this { actorsAggregate: { node: { name: this_actorsAggregate_var2 } } } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-label.test.ts b/packages/graphql/tests/tck/deprecated/labels-label.test.ts deleted file mode 100644 index a5fa6c7b36..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-label.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Cypher Aggregations Many while Alias fields", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Movie @node(label: "Film") { - id: ID! - title: String! - imdbRating: Int! - createdAt: DateTime! - } - - type Actor @node(additionalLabels: ["Person", "Alien"]) { - id: ID! - name: String! - imdbRating: Int! - createdAt: DateTime! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - }); - }); - - test("Custom Label Aggregations", async () => { - const query = gql` - { - moviesAggregate { - _id: id { - _shortest: shortest - _longest: longest - } - _title: title { - _shortest: shortest - _longest: longest - } - _imdbRating: imdbRating { - _min: min - _max: max - _average: average - } - _createdAt: createdAt { - _min: min - _max: max - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - RETURN { _id: { _shortest: min(this.id), _longest: max(this.id) }, _title: { _shortest: - reduce(aggVar = collect(this.title)[0], current IN collect(this.title) | - CASE - WHEN size(current) < size(aggVar) THEN current - ELSE aggVar - END - ) - , _longest: - reduce(aggVar = collect(this.title)[0], current IN collect(this.title) | - CASE - WHEN size(current) > size(aggVar) THEN current - ELSE aggVar - END - ) - }, _imdbRating: { _min: min(this.imdbRating), _max: max(this.imdbRating), _average: avg(this.imdbRating) }, _createdAt: { _min: apoc.date.convertFormat(toString(min(this.createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), _max: apoc.date.convertFormat(toString(max(this.createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") } }" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Additional Labels Aggregations", async () => { - const query = gql` - { - actorsAggregate { - _id: id { - _shortest: shortest - _longest: longest - } - _name: name { - _shortest: shortest - _longest: longest - } - _imdbRating: imdbRating { - _min: min - _max: max - _average: average - } - _createdAt: createdAt { - _min: min - _max: max - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Actor\`:\`Person\`:\`Alien\`) - RETURN { _id: { _shortest: min(this.id), _longest: max(this.id) }, _name: { _shortest: - reduce(aggVar = collect(this.name)[0], current IN collect(this.name) | - CASE - WHEN size(current) < size(aggVar) THEN current - ELSE aggVar - END - ) - , _longest: - reduce(aggVar = collect(this.name)[0], current IN collect(this.name) | - CASE - WHEN size(current) > size(aggVar) THEN current - ELSE aggVar - END - ) - }, _imdbRating: { _min: min(this.imdbRating), _max: max(this.imdbRating), _average: avg(this.imdbRating) }, _createdAt: { _min: apoc.date.convertFormat(toString(min(this.createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), _max: apoc.date.convertFormat(toString(max(this.createdAt)), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") } }" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-additional-labels.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-additional-labels.test.ts deleted file mode 100644 index 369b61fcc8..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-additional-labels.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Node directive with additionalLabels", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Actor @node(additionalLabels: ["Person"]) { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type Movie @node(label: "Film", additionalLabels: ["Multimedia"]) { - id: ID - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("Select Movie with additional labels", async () => { - const query = gql` - { - movies { - title - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`:\`Multimedia\`) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Select movie and actor with additional labels", async () => { - const query = gql` - { - movies { - title - actors { - name - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`:\`Multimedia\`) - CALL { - WITH this - MATCH (this_actors:\`Actor\`:\`Person\`)-[this0:ACTED_IN]->(this) - WITH this_actors { .name } AS this_actors - RETURN collect(this_actors) AS this_actors - } - RETURN this { .title, actors: this_actors } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Create Movie and relation with additional labels", async () => { - const query = gql` - mutation { - createMovies( - input: [ - { id: 1, actors: { create: [{ node: { name: "actor 1" } }] } } - { id: 2, actors: { create: [{ node: { name: "actor 2" } }] } } - ] - ) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "UNWIND $create_param0 AS create_var1 - CALL { - WITH create_var1 - CREATE (create_this0:\`Film\`:\`Multimedia\`) - SET - create_this0.id = create_var1.id - WITH create_this0, create_var1 - CALL { - WITH create_this0, create_var1 - UNWIND create_var1.actors.create AS create_var2 - WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 - CREATE (create_this5:\`Actor\`:\`Person\`) - SET - create_this5.name = create_var3.name - MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) - RETURN collect(NULL) AS create_var7 - } - RETURN create_this0 - } - RETURN collect(create_this0 { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"create_param0\\": [ - { - \\"id\\": \\"1\\", - \\"actors\\": { - \\"create\\": [ - { - \\"node\\": { - \\"name\\": \\"actor 1\\" - } - } - ] - } - }, - { - \\"id\\": \\"2\\", - \\"actors\\": { - \\"create\\": [ - { - \\"node\\": { - \\"name\\": \\"actor 2\\" - } - } - ] - } - } - ], - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Delete Movie with additional additionalLabels", async () => { - const query = gql` - mutation { - deleteMovies(where: { id: "123" }) { - nodesDeleted - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`:\`Multimedia\`) - WHERE this.id = $param0 - DETACH DELETE this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("Update Movie with additional labels", async () => { - const query = gql` - mutation { - updateMovies(where: { id: "1" }, update: { id: "2" }) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`:\`Multimedia\`) - WHERE this.id = $param0 - SET this.id = $this_update_id - RETURN collect(DISTINCT this { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"1\\", - \\"this_update_id\\": \\"2\\", - \\"resolvedCallbacks\\": {} - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-label-jwt.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-label-jwt.test.ts deleted file mode 100644 index fde32f6f33..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-label-jwt.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Label in Node directive", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Actor @node(additionalLabels: ["$jwt.personlabel"]) { - name: String - age: Int - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type Movie @node(label: "$jwt.movielabel") { - id: ID - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("Select Movie with label Film", async () => { - const query = gql` - query { - movies { - title - } - } - `; - - const req = createJwtRequest("secret", { movielabel: "Film" }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Select Movie with label Film from Actors with additionalLabels", async () => { - const query = gql` - query { - actors(where: { age_GT: 10 }) { - name - movies(where: { title: "terminator" }) { - title - } - } - } - `; - - const req = createJwtRequest("secret", { movielabel: "Film", personlabel: "Person" }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Actor\`:\`Person\`) - WHERE this.age > $param0 - CALL { - WITH this - MATCH (this)-[this0:ACTED_IN]->(this_movies:\`Film\`) - WHERE this_movies.title = $param1 - WITH this_movies { .title } AS this_movies - RETURN collect(this_movies) AS this_movies - } - RETURN this { .name, movies: this_movies } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 10, - \\"high\\": 0 - }, - \\"param1\\": \\"terminator\\" - }" - `); - }); - - test("Create Movie with label Film", async () => { - const query = gql` - mutation { - createMovies(input: { title: "Titanic" }) { - movies { - title - } - } - } - `; - - const req = createJwtRequest("secret", { movielabel: "Film" }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "UNWIND $create_param0 AS create_var1 - CALL { - WITH create_var1 - CREATE (create_this0:\`Film\`) - SET - create_this0.title = create_var1.title - RETURN create_this0 - } - RETURN collect(create_this0 { .title }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"create_param0\\": [ - { - \\"title\\": \\"Titanic\\" - } - ], - \\"resolvedCallbacks\\": {} - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-label-union.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-label-union.test.ts deleted file mode 100644 index 5a959c7a22..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-label-union.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Node directive with unions", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - union Search = Movie | Genre - - type Genre @node(label: "Category", additionalLabels: ["ExtraLabel1", "ExtraLabel2"]) { - name: String - } - - type Movie @node(label: "Film") { - title: String - search: [Search!]! @relationship(type: "SEARCH", direction: OUT) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("Read Unions", async () => { - const query = gql` - { - movies(where: { title: "some title" }) { - search( - where: { Movie: { title: "The Matrix" }, Genre: { name: "Horror" } } - options: { offset: 1, limit: 10 } - ) { - ... on Movie { - title - } - ... on Genre { - name - } - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.title = $param0 - CALL { - WITH this - CALL { - WITH * - MATCH (this)-[this0:SEARCH]->(this_search:\`Category\`:\`ExtraLabel1\`:\`ExtraLabel2\`) - WHERE this_search.name = $param1 - WITH this_search { __resolveType: \\"Genre\\", .name } AS this_search - RETURN this_search AS this_search - UNION - WITH * - MATCH (this)-[this1:SEARCH]->(this_search:\`Film\`) - WHERE this_search.title = $param2 - WITH this_search { __resolveType: \\"Movie\\", .title } AS this_search - RETURN this_search AS this_search - } - WITH this_search - SKIP $param3 - LIMIT $param4 - RETURN collect(this_search) AS this_search - } - RETURN this { search: this_search } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\", - \\"param1\\": \\"Horror\\", - \\"param2\\": \\"The Matrix\\", - \\"param3\\": { - \\"low\\": 1, - \\"high\\": 0 - }, - \\"param4\\": { - \\"low\\": 10, - \\"high\\": 0 - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-label.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-label.test.ts deleted file mode 100644 index 04a4e00014..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-label.test.ts +++ /dev/null @@ -1,583 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Label in Node directive", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Actor @node(label: "Person") { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type Movie @node(label: "Film") { - id: ID - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("Select Movie with label Film", async () => { - const query = gql` - { - movies { - title - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Select movie and actor with custom labels", async () => { - const query = gql` - { - movies { - title - actors { - name - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - CALL { - WITH this - MATCH (this_actors:\`Person\`)-[this0:ACTED_IN]->(this) - WITH this_actors { .name } AS this_actors - RETURN collect(this_actors) AS this_actors - } - RETURN this { .title, actors: this_actors } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Select movie and actor with custom labels using Relay connection", async () => { - const query = gql` - { - movies { - title - actorsConnection { - edges { - node { - name - } - } - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - CALL { - WITH this - MATCH (this)<-[this_connection_actorsConnectionthis0:ACTED_IN]-(this_Actor:\`Person\`) - WITH { node: { name: this_Actor.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this_actorsConnection - } - RETURN this { .title, actorsConnection: this_actorsConnection } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Create Movie with label Film", async () => { - const query = gql` - mutation { - createMovies(input: [{ id: "1" }]) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "UNWIND $create_param0 AS create_var1 - CALL { - WITH create_var1 - CREATE (create_this0:\`Film\`) - SET - create_this0.id = create_var1.id - RETURN create_this0 - } - RETURN collect(create_this0 { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"create_param0\\": [ - { - \\"id\\": \\"1\\" - } - ], - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Create Movie and relation with custom labels", async () => { - const query = gql` - mutation { - createMovies( - input: [ - { id: 1, actors: { create: [{ node: { name: "actor 1" } }] } } - { id: 2, actors: { create: [{ node: { name: "actor 2" } }] } } - ] - ) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "UNWIND $create_param0 AS create_var1 - CALL { - WITH create_var1 - CREATE (create_this0:\`Film\`) - SET - create_this0.id = create_var1.id - WITH create_this0, create_var1 - CALL { - WITH create_this0, create_var1 - UNWIND create_var1.actors.create AS create_var2 - WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 - CREATE (create_this5:\`Person\`) - SET - create_this5.name = create_var3.name - MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) - RETURN collect(NULL) AS create_var7 - } - RETURN create_this0 - } - RETURN collect(create_this0 { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"create_param0\\": [ - { - \\"id\\": \\"1\\", - \\"actors\\": { - \\"create\\": [ - { - \\"node\\": { - \\"name\\": \\"actor 1\\" - } - } - ] - } - }, - { - \\"id\\": \\"2\\", - \\"actors\\": { - \\"create\\": [ - { - \\"node\\": { - \\"name\\": \\"actor 2\\" - } - } - ] - } - } - ], - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Update Movie with label film", async () => { - const query = gql` - mutation { - updateMovies(where: { id: "1" }, update: { id: "2" }) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - SET this.id = $this_update_id - RETURN collect(DISTINCT this { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"1\\", - \\"this_update_id\\": \\"2\\", - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Update nested actors with custom label", async () => { - const query = gql` - mutation { - updateMovies( - where: { id: "1" } - update: { - actors: [{ where: { node: { name: "old name" } }, update: { node: { name: "new name" } } }] - } - ) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - WITH this - CALL { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:\`Person\`) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_actors0.name = $this_update_actors0_name - RETURN count(*) AS update_this_actors0 - } - RETURN collect(DISTINCT this { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"old name\\", - \\"this_update_actors0_name\\": \\"new name\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": \\"old name\\" - } - }, - \\"update\\": { - \\"node\\": { - \\"name\\": \\"new name\\" - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Update connection in Movie with label film", async () => { - const query = gql` - mutation { - updateMovies(where: { id: "1" }, connect: { actors: [{ where: { node: { name: "Daniel" } } }] }) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - WITH this - CALL { - WITH this - OPTIONAL MATCH (this_connect_actors0_node:\`Person\`) - WHERE this_connect_actors0_node.name = $this_connect_actors0_node_param0 - CALL { - WITH * - WITH collect(this_connect_actors0_node) as connectedNodes, collect(this) as parentNodes - CALL { - WITH connectedNodes, parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_actors0_node - MERGE (this)<-[:ACTED_IN]-(this_connect_actors0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS _ - } - WITH this, this_connect_actors0_node - RETURN count(*) AS connect_this_connect_actors_Actor - } - WITH * - RETURN collect(DISTINCT this { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"1\\", - \\"this_connect_actors0_node_param0\\": \\"Daniel\\", - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Update disconnect in Movie with label film", async () => { - const query = gql` - mutation { - updateMovies(where: { id: "1" }, disconnect: { actors: [{ where: { node: { name: "Daniel" } } }] }) { - movies { - id - } - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - WITH this - CALL { - WITH this - OPTIONAL MATCH (this)<-[this_disconnect_actors0_rel:ACTED_IN]-(this_disconnect_actors0:\`Person\`) - WHERE this_disconnect_actors0.name = $updateMovies_args_disconnect_actors0_where_this_disconnect_actors0param0 - CALL { - WITH this_disconnect_actors0, this_disconnect_actors0_rel, this - WITH collect(this_disconnect_actors0) as this_disconnect_actors0, this_disconnect_actors0_rel, this - UNWIND this_disconnect_actors0 as x - DELETE this_disconnect_actors0_rel - RETURN count(*) AS _ - } - RETURN count(*) AS disconnect_this_disconnect_actors_Actor - } - WITH * - RETURN collect(DISTINCT this { .id }) AS data" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"1\\", - \\"updateMovies_args_disconnect_actors0_where_this_disconnect_actors0param0\\": \\"Daniel\\", - \\"updateMovies\\": { - \\"args\\": { - \\"disconnect\\": { - \\"actors\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": \\"Daniel\\" - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} - }" - `); - }); - - test("Delete Movie with custom label", async () => { - const query = gql` - mutation { - deleteMovies(where: { id: "123" }) { - nodesDeleted - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - DETACH DELETE this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("Delete Movies and actors with custom labels", async () => { - const query = gql` - mutation { - deleteMovies(where: { id: 123 }, delete: { actors: { where: { node: { name: "Actor to delete" } } } }) { - nodesDeleted - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE this.id = $param0 - WITH this - OPTIONAL MATCH (this)<-[this_actors0_relationship:ACTED_IN]-(this_actors0:\`Person\`) - WHERE this_actors0.name = $this_deleteMovies_args_delete_actors0_where_this_actors0param0 - WITH this, collect(DISTINCT this_actors0) AS this_actors0_to_delete - CALL { - WITH this_actors0_to_delete - UNWIND this_actors0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ - } - DETACH DELETE this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\", - \\"this_deleteMovies\\": { - \\"args\\": { - \\"delete\\": { - \\"actors\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": \\"Actor to delete\\" - } - } - } - ] - } - } - }, - \\"this_deleteMovies_args_delete_actors0_where_this_actors0param0\\": \\"Actor to delete\\" - }" - `); - }); - - test("Admin Deletes Post", async () => { - const query = gql` - mutation { - deleteMovies(where: { actors: { name: "tom" } }) { - nodesDeleted - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Film\`) - WHERE EXISTS { - MATCH (this0:\`Person\`)-[:ACTED_IN]->(this) - WHERE this0.name = $param0 - } - DETACH DELETE this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"tom\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-labels.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-labels.test.ts deleted file mode 100644 index c43522a6cc..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-labels.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Cypher -> fulltext -> Additional Labels", () => { - test("simple match with single fulltext property and static additionalLabels", async () => { - const typeDefs = gql` - type Movie - @fulltext(indexes: [{ name: "MovieTitle", fields: ["title"] }]) - @node(additionalLabels: ["AnotherLabel"]) { - title: String - } - `; - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - - const query = gql` - query { - movies(fulltext: { MovieTitle: { phrase: "something AND something" } }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query, {}); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE (\\"Movie\\" IN labels(this) AND \\"AnotherLabel\\" IN labels(this)) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"something AND something\\" - }" - `); - }); - - test("simple match with single fulltext property and jwt additionalLabels", async () => { - const typeDefs = gql` - type Movie - @fulltext(indexes: [{ name: "MovieTitle", fields: ["title"] }]) - @node(additionalLabels: ["$jwt.label"]) { - title: String - } - `; - - const label = "some-label"; - - const secret = "supershhhhhh"; - - const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - - const query = gql` - query { - movies(fulltext: { MovieTitle: { phrase: "something AND something" } }) { - title - } - } - `; - - const req = createJwtRequest(secret, { label }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE (\\"Movie\\" IN labels(this) AND \\"some-label\\" IN labels(this)) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"something AND something\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-with-auth-projection.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-with-auth-projection.test.ts deleted file mode 100644 index e5a9663c0a..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-with-auth-projection.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Cypher Auth Projection On Connections", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Post @node(label: "Comment") { - content: String - creator: User! @relationship(type: "HAS_POST", direction: IN) - } - - type User @node(label: "Person") { - id: ID - name: String - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT) - } - - extend type User @auth(rules: [{ allow: { id: "$jwt.sub" } }]) - extend type Post @auth(rules: [{ allow: { creator: { id: "$jwt.sub" } } }]) - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("One connection", async () => { - const query = gql` - { - users { - name - postsConnection { - edges { - node { - content - } - } - } - } - } - `; - - const req = createJwtRequest("secret", { sub: "super_admin" }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Person\`) - WHERE apoc.util.validatePredicate(NOT ((this.id IS NOT NULL AND this.id = $param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH this - MATCH (this)-[this_connection_postsConnectionthis0:HAS_POST]->(this_Post:\`Comment\`) - WHERE apoc.util.validatePredicate(NOT ((exists((this_Post)<-[:HAS_POST]-(:\`Person\`)) AND any(this_connection_postsConnectionthis1 IN [(this_Post)<-[:HAS_POST]-(this_connection_postsConnectionthis1:\`Person\`) | this_connection_postsConnectionthis1] WHERE (this_connection_postsConnectionthis1.id IS NOT NULL AND this_connection_postsConnectionthis1.id = $this_connection_postsConnectionparam0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { content: this_Post.content } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this_postsConnection - } - RETURN this { .name, postsConnection: this_postsConnection } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"super_admin\\", - \\"this_connection_postsConnectionparam0\\": \\"super_admin\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node-with-auth.test.ts b/packages/graphql/tests/tck/deprecated/labels-node-with-auth.test.ts deleted file mode 100644 index f72cfe5793..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node-with-auth.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; -import { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Node Directive", () => { - const secret = "secret"; - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type Post @node(label: "Comment") { - id: ID - content: String - creator: User! @relationship(type: "HAS_POST", direction: IN) - } - - extend type Post @auth(rules: [{ operations: [DELETE], roles: ["admin"] }]) - - type User @node(label: "Person") { - id: ID - name: String - posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT) - } - - extend type User - @auth(rules: [{ operations: [READ, UPDATE, DELETE, DISCONNECT, CONNECT], allow: { id: "$jwt.sub" } }]) - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, - }); - }); - - test("Read User", async () => { - const query = gql` - { - users { - id - } - } - `; - - const req = createJwtRequest("secret", { sub: "id-01", roles: ["admin"] }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Person\`) - WHERE apoc.util.validatePredicate(NOT ((this.id IS NOT NULL AND this.id = $param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"id-01\\" - }" - `); - }); - - test("Admin Deletes Post", async () => { - const query = gql` - mutation { - deletePosts(where: { creator: { id: "123" } }) { - nodesDeleted - } - } - `; - - const req = createJwtRequest("secret", { sub: "id-01", roles: ["admin"] }); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`Comment\`) - WHERE single(this0 IN [(this0:\`Person\`)-[:HAS_POST]->(this) | this0] WHERE this0.id = $param0) - WITH this - CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - DETACH DELETE this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\", - \\"auth\\": { - \\"isAuthenticated\\": true, - \\"roles\\": [ - \\"admin\\" - ], - \\"jwt\\": { - \\"roles\\": [ - \\"admin\\" - ], - \\"sub\\": \\"id-01\\" - } - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/deprecated/labels-node.test.ts b/packages/graphql/tests/tck/deprecated/labels-node.test.ts deleted file mode 100644 index 0f7bae3ef3..0000000000 --- a/packages/graphql/tests/tck/deprecated/labels-node.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { gql } from "apollo-server"; -import type { DocumentNode } from "graphql"; -import { Neo4jGraphQL } from "../../../src"; -import { createJwtRequest } from "../../utils/create-jwt-request"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("Cypher Where Aggregations with @node directive", () => { - let typeDefs: DocumentNode; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = gql` - type User @node(label: "_User", additionalLabels: ["additionalUser"]) { - someName: String - } - - type Post @node(label: "_Post", additionalLabels: ["additionalPost"]) { - content: String! - likes: [User!]! @relationship(type: "LIKES", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - config: { enableRegex: true }, - }); - }); - - test("GT with node directive", async () => { - const query = gql` - { - posts(where: { likesAggregate: { node: { someName_GT: 1 } } }) { - content - } - } - `; - - const req = createJwtRequest("secret", {}); - const result = await translateQuery(neoSchema, query, { - req, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:\`_Post\`:\`additionalPost\`) - CALL { - WITH this - MATCH (this1:\`_User\`:\`additionalUser\`)-[this0:LIKES]->(this) - RETURN any(var2 IN collect(size(this1.someName)) WHERE var2 > $param0) AS var3 - } - WITH * - WHERE var3 = true - RETURN this { .content } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 1, - \\"high\\": 0 - } - }" - `); - }); -}); diff --git a/packages/introspector/src/transforms/neo4j-graphql/directives/Node.ts b/packages/introspector/src/transforms/neo4j-graphql/directives/Node.ts index f65813b2d3..1b47d9ec20 100644 --- a/packages/introspector/src/transforms/neo4j-graphql/directives/Node.ts +++ b/packages/introspector/src/transforms/neo4j-graphql/directives/Node.ts @@ -20,28 +20,16 @@ import type { Directive } from "../types"; export class NodeDirective implements Directive { - label?: string; - additionalLabels: string[] = []; + labels: string[] = []; - addLabel(label: string): void { - this.label = label; - } - - addAdditionalLabels(labels: string[] | string): void { + addLabels(labels: string[]): void { if (!labels.length) { return; } - this.additionalLabels = this.additionalLabels.concat(labels); + this.labels = this.labels.concat(labels); } toString(): string { - const directiveArguments: string[] = []; - if (this.label) { - directiveArguments.push(`label: "${this.label}"`); - } - if (this.additionalLabels.length) { - directiveArguments.push(`additionalLabels: ["${this.additionalLabels.join('","')}"]`); - } - return directiveArguments.length ? `@node(${directiveArguments.join(", ")})` : ""; + return this.labels.length ? `@node(labels: ["${this.labels.join('", "')}"])` : ""; } } diff --git a/packages/introspector/src/transforms/neo4j-graphql/graphql.ts b/packages/introspector/src/transforms/neo4j-graphql/graphql.ts index 35a80ee470..d0af08a9b8 100644 --- a/packages/introspector/src/transforms/neo4j-graphql/graphql.ts +++ b/packages/introspector/src/transforms/neo4j-graphql/graphql.ts @@ -58,13 +58,13 @@ function transformNodes(nodes: NodeMap, readonly: boolean): GraphQLNodeMap { const uniqueTypeName = uniqueString(typeName, takenTypeNames); takenTypeNames.push(uniqueTypeName); - const node = new GraphQLNode("type", uniqueTypeName); const nodeDirective = new NodeDirective(); - if (mainLabel !== uniqueTypeName) { - nodeDirective.addLabel(mainLabel); + + if (neo4jNode.labels.length > 1 || mainLabel !== uniqueTypeName) { + nodeDirective.addLabels(neo4jNode.labels); } - nodeDirective.addAdditionalLabels(neo4jNode.labels.slice(1)); + if (nodeDirective.toString().length) { node.addDirective(nodeDirective); } diff --git a/packages/introspector/tests/integration/graphql/graphs.test.ts b/packages/introspector/tests/integration/graphql/graphs.test.ts index 95766f635d..c593dc7a75 100644 --- a/packages/introspector/tests/integration/graphql/graphs.test.ts +++ b/packages/introspector/tests/integration/graphql/graphs.test.ts @@ -144,14 +144,14 @@ describe("GraphQL - Infer Schema on graphs", () => { const typeDefs = await toGraphQLTypeDefs(sessionFactory(bm)); expect(typeDefs).toMatchInlineSnapshot(` - "type Actor @node(additionalLabels: [\\"Person\\"]) { + "type Actor @node(labels: [\\"Actor\\", \\"Person\\"]) { actedInMovies: [Movie!]! @relationship(type: \\"ACTED_IN\\", direction: OUT) actedInPlays: [Play!]! @relationship(type: \\"ACTED_IN\\", direction: OUT) directedMovies: [Movie!]! @relationship(type: \\"DIRECTED\\", direction: OUT) name: String! } - type Dog @node(additionalLabels: [\\"K9\\"]) { + type Dog @node(labels: [\\"Dog\\", \\"K9\\"]) { actedInMovies: [Movie!]! @relationship(type: \\"ACTED_IN\\", direction: OUT) name: String! } @@ -163,7 +163,7 @@ describe("GraphQL - Infer Schema on graphs", () => { title: String! } - type Play @node(additionalLabels: [\\"Theater\\"]) { + type Play @node(labels: [\\"Play\\", \\"Theater\\"]) { actorsActedIn: [Actor!]! @relationship(type: \\"ACTED_IN\\", direction: IN) title: String! }" @@ -270,13 +270,13 @@ describe("GraphQL - Infer Schema on graphs", () => { roles: [String]! } - type Actor_Label @node(label: \\"Actor-Label\\") { + type Actor_Label @node(labels: [\\"Actor-Label\\"]) { actedInMovieLabels: [Movie_Label!]! @relationship(type: \\"ACTED-IN\\", direction: OUT, properties: \\"ActedInProperties\\") movieLabelsWonPrizeFor: [Movie_Label!]! @relationship(type: \\"WON_PRIZE_FOR\\", direction: IN) name: String! } - type Movie_Label @node(label: \\"Movie-Label\\") { + type Movie_Label @node(labels: [\\"Movie-Label\\"]) { actorLabelsActedIn: [Actor_Label!]! @relationship(type: \\"ACTED-IN\\", direction: IN, properties: \\"ActedInProperties\\") title: String! wonPrizeForActorLabels: [Actor_Label!]! @relationship(type: \\"WON_PRIZE_FOR\\", direction: OUT) diff --git a/packages/introspector/tests/integration/graphql/nodes.test.ts b/packages/introspector/tests/integration/graphql/nodes.test.ts index 3deb21df70..3a75d9bb50 100644 --- a/packages/introspector/tests/integration/graphql/nodes.test.ts +++ b/packages/introspector/tests/integration/graphql/nodes.test.ts @@ -197,7 +197,7 @@ describe("GraphQL - Infer Schema nodes basic tests", () => { strProp: String! } - type TestLabel2 @node(additionalLabels: [\\"TestLabel3\\"]) { + type TestLabel2 @node(labels: [\\"TestLabel2\\", \\"TestLabel3\\"]) { singleProp: BigInt! }" `); @@ -227,11 +227,11 @@ describe("GraphQL - Infer Schema nodes basic tests", () => { const typeDefs = await toGraphQLTypeDefs(sessionFactory(bm)); expect(typeDefs).toMatchInlineSnapshot(` - "type Test_Label @node(label: \\"Test\`Label\\") { + "type Test_Label @node(labels: [\\"Test\`Label\\"]) { strProp: String! } - type Test_Label2 @node(label: \\"Test-Label\\") { + type Test_Label2 @node(labels: [\\"Test-Label\\"]) { singleProp: BigInt! }" `); @@ -256,7 +256,7 @@ describe("GraphQL - Infer Schema nodes basic tests", () => { const typeDefs = await toGraphQLTypeDefs(sessionFactory(bm)); expect(typeDefs).toMatchInlineSnapshot(` - "type _2number @node(label: \\"2number\\") { + "type _2number @node(labels: [\\"2number\\"]) { prop: BigInt! }" `); @@ -380,7 +380,7 @@ describe("GraphQL - Infer Schema nodes basic tests", () => { strProp: String! } - type TestLabel2 @node(additionalLabels: [\\"TestLabel3\\"]) @exclude(operations: [CREATE, DELETE, UPDATE]) { + type TestLabel2 @node(labels: [\\"TestLabel2\\", \\"TestLabel3\\"]) @exclude(operations: [CREATE, DELETE, UPDATE]) { singleProp: BigInt! }" `);