diff --git a/.changeset/good-wombats-live.md b/.changeset/good-wombats-live.md new file mode 100644 index 0000000000..04556a2ec8 --- /dev/null +++ b/.changeset/good-wombats-live.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Fix duplicate relationships on nested connect from union types diff --git a/docs/contributing/DEVELOPING.md b/docs/contributing/DEVELOPING.md index bca85679f9..3107504435 100644 --- a/docs/contributing/DEVELOPING.md +++ b/docs/contributing/DEVELOPING.md @@ -56,9 +56,9 @@ yarn install [Visual Studio Code](https://code.visualstudio.com/) comes highly recommended for working in this repository, and we additionally recommend the following extensions: -* [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) -* [Jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) -* [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) +- [Jest](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) +- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) The Jest extension should automatically detect the tests for this repository and watch them in the Status Bar. @@ -69,16 +69,16 @@ In some cases, it may be worth testing `@neo4j/graphql` with a local version of In the Cypher Builder folder run: -* `yarn link` -* `yarn build` - This step needs to be done each time a change is done in the Cypher Builder +- `yarn link` +- `yarn build` - This step needs to be done each time a change is done in the Cypher Builder In the root of the `neo4j/graphql` monorepo run: -* `yarn link -p [path-to-local-cypher-builder] +- `yarn link -p [path-to-local-cypher-builder] To unlink, in the graphql project: -* `yarn unlink @neo4j/cypher-builder` +- `yarn unlink @neo4j/cypher-builder` ## Testing @@ -135,7 +135,7 @@ yarn test:tck You can run all the TCK tests against the database to check that the Cypher generated is valid. This can be done with the env variable `VERIFY_TCK` ```bash -VERIFY_TCK yarn test:tck +VERIFY_TCK=true yarn test:tck ``` ### Testing using docker @@ -228,8 +228,8 @@ adhere to our linting and formatting rules. For the sake of completeness, add an entry for the new project into the following files in the root of the repository: -* `tsconfig.json` (`references` entry) -* `jest.config.base.js` (`moduleNameMapper` entry) +- `tsconfig.json` (`references` entry) +- `jest.config.base.js` (`moduleNameMapper` entry) ### Dependencies within the monorepo @@ -268,10 +268,10 @@ like: The real key entries here are: -* `baseUrl` - for all of the relative references in this file, this will tell - `tsc` where to start from -* `paths` - this will translate `import` statements in code to the relative dependency -* `references` - gives TypeScript "permission" to accesss the projects at these paths +- `baseUrl` - for all of the relative references in this file, this will tell + `tsc` where to start from +- `paths` - this will translate `import` statements in code to the relative dependency +- `references` - gives TypeScript "permission" to accesss the projects at these paths Finally, it is highly likely that Jest will also need access to this internal dependency, so `packages/project/jest.config.js` will need to look like: diff --git a/packages/graphql/src/classes/CallbackBucketDeprecated.ts b/packages/graphql/src/classes/CallbackBucketDeprecated.ts deleted file mode 100644 index ef0d806cb3..0000000000 --- a/packages/graphql/src/classes/CallbackBucketDeprecated.ts +++ /dev/null @@ -1,145 +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 { GraphQLBoolean, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; -import type { DateTime, Duration, Integer, LocalDateTime, LocalTime, Date as Neo4jDate, Time } from "neo4j-driver"; -import { - GraphQLBigInt, - GraphQLDate, - GraphQLDateTime, - GraphQLDuration, - GraphQLLocalDateTime, - GraphQLLocalTime, - GraphQLTime, -} from "../graphql/scalars"; -import type { Neo4jGraphQLCallbacks, TypeMeta } from "../types"; -import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; - -interface Callback { - functionName: string; - paramName: string; - parent?: Record; - type: TypeMeta; -} - -type CallbackResult = - | number - | string - | boolean - | Integer - | DateTime - | DateTime - | Neo4jDate - | Neo4jDate - | Time - | Time - | LocalDateTime - | LocalTime - | LocalTime - | Duration - | Duration - | Array; - -/** @deprecated */ -export class CallbackBucketDeprecated { - public callbacks: Callback[]; - private context: Neo4jGraphQLTranslationContext; - - constructor(context: Neo4jGraphQLTranslationContext) { - this.context = context; - this.callbacks = []; - } - - public addCallback(callback: Callback): void { - this.callbacks.push(callback); - } - - public async resolveCallbacksAndFilterCypher(options: { - cypher: string; - }): Promise<{ cypher: string; params: Record }> { - const params: Record = {}; - let cypher = options.cypher; - - await Promise.all( - this.callbacks.map(async (cb) => { - const callbackFunction = (this.context.features.populatedBy?.callbacks as Neo4jGraphQLCallbacks)[ - cb.functionName - ] as ( - parent?: Record, - args?: Record, - context?: Neo4jGraphQLContext - ) => Promise; - const param = await callbackFunction(cb.parent, {}, this.context); - - if (param === undefined) { - cypher = cypher - .split("\n") - .filter((line) => !line.includes(`$resolvedCallbacks.${cb.paramName}`)) - .join("\n"); - } else if (param === null) { - params[cb.paramName] = null; - } else { - params[cb.paramName] = this.parseCallbackResult(param, cb.type); - } - }) - ); - - return { cypher, params }; - } - - private parseCallbackResult(result: unknown, type: TypeMeta): CallbackResult { - if (type.array) { - if (!Array.isArray(result)) { - throw new GraphQLError("Expected list as callback result but did not."); - } - - return result.map((r) => this.parseCallbackResult(r, { ...type, array: false })); - } - - switch (type.name) { - case "Int": - return GraphQLInt.parseValue(result); - case "Float": - return GraphQLFloat.parseValue(result); - case "String": - return GraphQLString.parseValue(result); - case "Boolean": - return GraphQLBoolean.parseValue(result); - case "ID": - return GraphQLID.parseValue(result); - case "BigInt": - return GraphQLBigInt.parseValue(result); - case "DateTime": - return GraphQLDateTime.parseValue(result); - case "Date": - return GraphQLDate.parseValue(result); - case "Time": - return GraphQLTime.parseValue(result); - case "LocalDateTime": - return GraphQLLocalDateTime.parseValue(result); - case "LocalTime": - return GraphQLLocalTime.parseValue(result); - case "Duration": - return GraphQLDuration.parseValue(result); - default: - throw new GraphQLError("Callback result received for field of unsupported type."); - } - } -} diff --git a/packages/graphql/src/classes/GraphElement.ts b/packages/graphql/src/classes/GraphElement.ts deleted file mode 100644 index 1eaf3a8ca0..0000000000 --- a/packages/graphql/src/classes/GraphElement.ts +++ /dev/null @@ -1,87 +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 { - CypherField, - PrimitiveField, - CustomEnumField, - CustomScalarField, - TemporalField, - PointField, - CustomResolverField, - BaseField, -} from "../types"; - -export interface GraphElementConstructor { - name: string; - description?: string; - cypherFields: CypherField[]; - primitiveFields: PrimitiveField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - customResolverFields: CustomResolverField[]; -} - -export abstract class GraphElement { - public name: string; - public description?: string; - public primitiveFields: PrimitiveField[]; - public scalarFields: CustomScalarField[]; - public enumFields: CustomEnumField[]; - public temporalFields: TemporalField[]; - public pointFields: PointField[]; - public customResolverFields: CustomResolverField[]; - - constructor(input: GraphElementConstructor) { - this.name = input.name; - this.description = input.description; - this.primitiveFields = input.primitiveFields; - this.scalarFields = input.scalarFields; - this.enumFields = input.enumFields; - this.temporalFields = input.temporalFields; - this.pointFields = input.pointFields; - this.customResolverFields = input.customResolverFields; - } - - public getField(name: string): BaseField | undefined { - for (const fieldList of this.getAllFields()) { - const field = this.searchFieldInList(name, fieldList); - if (field) return field; - } - } - - private getAllFields(): Array { - return [ - this.primitiveFields, - this.scalarFields, - this.enumFields, - this.temporalFields, - this.pointFields, - this.customResolverFields, - ]; - } - - private searchFieldInList(fieldName: string, fields: BaseField[]): BaseField | undefined { - return fields.find((field) => { - return field.fieldName === fieldName; - }); - } -} diff --git a/packages/graphql/src/classes/LimitDirective.test.ts b/packages/graphql/src/classes/LimitDirective.test.ts deleted file mode 100644 index 45ab35531f..0000000000 --- a/packages/graphql/src/classes/LimitDirective.test.ts +++ /dev/null @@ -1,67 +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 * as neo4j from "neo4j-driver"; -import { LimitDirective } from "./LimitDirective"; - -describe("QueryOptionsDirective", () => { - describe("getLimit", () => { - test("should return default limit", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit()).toEqual(neo4j.int(5)); - }); - - test("should return max limit if default is not available", () => { - const limit = new LimitDirective({ - max: neo4j.int(8), - }); - - expect(limit.getLimit()).toEqual(neo4j.int(8)); - }); - - test("should override default limit", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit(neo4j.int(2))).toEqual(neo4j.int(2)); - expect(limit.getLimit(neo4j.int(6))).toEqual(neo4j.int(6)); - }); - - test("should return if a higher one is given max limit if a higher one is given", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit(neo4j.int(16))).toEqual(neo4j.int(8)); - }); - - test("should return undefined if no limit is given", () => { - const limit = new LimitDirective({}); - - expect(limit.getLimit()).toBeUndefined(); - }); - }); -}); diff --git a/packages/graphql/src/classes/LimitDirective.ts b/packages/graphql/src/classes/LimitDirective.ts deleted file mode 100644 index 6fd3e591eb..0000000000 --- a/packages/graphql/src/classes/LimitDirective.ts +++ /dev/null @@ -1,48 +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 * as neo4j from "neo4j-driver"; -import type { Integer } from "neo4j-driver"; - -type LimitDirectiveConstructor = { - default?: Integer; - max?: Integer; -}; - -export class LimitDirective { - private default?: Integer; - private max?: Integer; - - constructor(limit: LimitDirectiveConstructor) { - this.default = limit.default; - this.max = limit.max; - } - - public getLimit(optionsLimit?: Integer | number): Integer | undefined { - if (optionsLimit) { - const integerLimit = neo4j.int(optionsLimit); - if (this.max && integerLimit.greaterThan(this.max)) { - return this.max; - } - return integerLimit; - } - - return this.default || this.max; - } -} diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index c1669dc8ac..006b57fc39 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -46,8 +46,6 @@ import type { ExecutorConstructorParam, Neo4jGraphQLSessionConfig } from "./Exec import { Executor } from "./Executor"; import type { Neo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; import { getNeo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; -import type Node from "./Node"; -import type Relationship from "./Relationship"; import { Neo4jGraphQLAuthorization } from "./authorization/Neo4jGraphQLAuthorization"; import { Neo4jGraphQLSubscriptionsCDCEngine } from "./subscription/Neo4jGraphQLSubscriptionsCDCEngine"; import { assertIndexesAndConstraints } from "./utils/asserts-indexes-and-constraints"; @@ -72,9 +70,6 @@ class Neo4jGraphQL { private driver?: Driver; private features: ContextFeatures; - private _nodes?: Node[]; - private _relationships?: Relationship[]; - private jwtFieldsMap?: Map; private schemaModel?: Neo4jGraphQLSchemaModel; @@ -201,22 +196,6 @@ class Neo4jGraphQL { }); } - private get nodes(): Node[] { - if (!this._nodes) { - throw new Error("You must await `.getSchema()` before accessing `nodes`"); - } - - return this._nodes; - } - - private get relationships(): Relationship[] { - if (!this._relationships) { - throw new Error("You must await `.getSchema()` before accessing `relationships`"); - } - - return this._relationships; - } - /** * Currently just merges all type definitions into a document. Eventual intention described below: * @@ -309,8 +288,6 @@ class Neo4jGraphQL { const wrapResolverArgs: WrapResolverArguments = { driver: this.driver, - nodes: this.nodes, - relationships: this.relationships, schemaModel: this.schemaModel, features: this.features, authorization: this.authorization, @@ -382,7 +359,7 @@ class Neo4jGraphQL { private generateSchemaModel(document: DocumentNode): Neo4jGraphQLSchemaModel { if (!this.schemaModel) { - return generateModel(document); + return generateModel(document, this.resolvers); } return this.schemaModel; } @@ -420,10 +397,9 @@ class Neo4jGraphQL { this.schemaModel = this.generateSchemaModel(document); - const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema({ + const { typeDefs, resolvers } = makeAugmentedSchema({ document, features: this.features, - userCustomResolvers: this.resolvers, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, }); @@ -437,9 +413,6 @@ class Neo4jGraphQL { }); } - this._nodes = nodes; - this._relationships = relationships; - const schema = makeExecutableSchema({ typeDefs, resolvers, @@ -490,10 +463,9 @@ class Neo4jGraphQL { this.schemaModel = this.generateSchemaModel(document); - const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema({ + const { typeDefs, resolvers } = makeAugmentedSchema({ document, features: this.features, - userCustomResolvers: this.resolvers, subgraph, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, @@ -510,9 +482,6 @@ class Neo4jGraphQL { }); } - this._nodes = nodes; - this._relationships = relationships; - // TODO: Move into makeAugmentedSchema, add resolvers alongside other resolvers const referenceResolvers = subgraph.getReferenceResolvers(this.schemaModel); diff --git a/packages/graphql/src/classes/Node.test.ts b/packages/graphql/src/classes/Node.test.ts deleted file mode 100644 index 95ed8565bd..0000000000 --- a/packages/graphql/src/classes/Node.test.ts +++ /dev/null @@ -1,880 +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 { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import type { MutationResponseTypeNames, NodeConstructor, RootTypeFieldNames, SubscriptionEvents } from "./Node"; -import Node from "./Node"; -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", - }, - }, - ], - [ - "AWSAccount", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "AWS_ACCOUNT", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "aws-account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "aws_account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "account", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - }, - }, - ], - [ - "ACCOUNT", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - }, - }, - ], - [ - "A", - { - create: "createAs", - read: "as", - update: "updateAs", - delete: "deleteAs", - aggregate: "asAggregate", - subscribe: { - created: "aCreated", - updated: "aUpdated", - deleted: "aDeleted", - }, - }, - ], - [ - "_2number", - { - create: "create_2Numbers", - read: "_2Numbers", - update: "update_2Numbers", - delete: "delete_2Numbers", - aggregate: "_2NumbersAggregate", - subscribe: { - created: "_2NumberCreated", - updated: "_2NumberUpdated", - deleted: "_2NumberDeleted", - }, - }, - ], - [ - "__2number", - { - create: "create__2Numbers", - read: "__2Numbers", - update: "update__2Numbers", - delete: "delete__2Numbers", - aggregate: "__2NumbersAggregate", - subscribe: { - created: "__2NumberCreated", - updated: "__2NumberUpdated", - deleted: "__2NumberDeleted", - }, - }, - ], - [ - "_number", - { - create: "create_numbers", - read: "_numbers", - update: "update_numbers", - delete: "delete_numbers", - aggregate: "_numbersAggregate", - subscribe: { - created: "_numberCreated", - updated: "_numberUpdated", - deleted: "_numberDeleted", - }, - }, - ], - ])("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", - }, - }, - ], - [ - "AWSAccounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "AWS_ACCOUNTS", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "aws-accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "aws_accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "accounts", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "ACCOUNTS", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - ])( - "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({ - labels: ["$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - }) - .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({ - labels: ["$context.myKey"], - }) - .instance(); - - const context = new ContextBuilder().instance(); - - const labels = node.getLabels({ ...context, myKey: "Movie" } as Record); - const labelString = node.getLabelString({ ...context, myKey: "Movie" } as Record); - - 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({ - labels: ["Film", "$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Film", "Movie"]); - expect(labelString).toBe(":Film:Movie"); - }); - }); -}); diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts deleted file mode 100644 index 6a6b349f30..0000000000 --- a/packages/graphql/src/classes/Node.ts +++ /dev/null @@ -1,297 +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 camelcase from "camelcase"; -import type { DirectiveNode, NamedTypeNode } from "graphql"; -import pluralize from "pluralize"; -import type { - ConnectionField, - CustomEnumField, - CustomScalarField, - CypherField, - FullText, - InterfaceField, - ObjectField, - PointField, - PrimitiveField, - RelationField, - TemporalField, - UnionField, -} from "../types"; -import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; -import type { DecodedGlobalId } from "../utils/global-ids"; -import { fromGlobalId, toGlobalId } from "../utils/global-ids"; -import { leadingUnderscores } from "../utils/leading-underscore"; -import { upperFirst } from "../utils/upper-first"; -import type { GraphElementConstructor } from "./GraphElement"; -import { GraphElement } from "./GraphElement"; -import type { LimitDirective } from "./LimitDirective"; -import type { NodeDirective } from "./NodeDirective"; - -export interface NodeConstructor extends GraphElementConstructor { - name: string; - relationFields: RelationField[]; - connectionFields: ConnectionField[]; - cypherFields: CypherField[]; - primitiveFields: PrimitiveField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - otherDirectives: DirectiveNode[]; - propagatedDirectives: DirectiveNode[]; - unionFields: UnionField[]; - interfaceFields: InterfaceField[]; - interfaces: NamedTypeNode[]; - objectFields: ObjectField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - plural?: string; - fulltextDirective?: FullText; - nodeDirective?: NodeDirective; - description?: string; - limitDirective?: LimitDirective; - isGlobalNode?: boolean; - globalIdField?: string; - globalIdFieldIsInt?: boolean; -} - -export type MutableField = - | PrimitiveField - | CustomScalarField - | CustomEnumField - | UnionField - | TemporalField - | CypherField; - -export type RootTypeFieldNames = { - create: string; - read: string; - update: string; - delete: string; - aggregate: string; - subscribe: { - created: string; - updated: string; - deleted: string; - }; -}; - -export type AggregateTypeNames = { - selection: string; - input: string; -}; - -export type MutationResponseTypeNames = { - create: string; - update: string; -}; - -export type SubscriptionEvents = { - create: string; - update: string; - delete: string; - create_relationship: string; - delete_relationship: string; -}; - -class Node extends GraphElement { - public relationFields: RelationField[]; - public connectionFields: ConnectionField[]; - public cypherFields: CypherField[]; - public otherDirectives: DirectiveNode[]; - public propagatedDirectives: DirectiveNode[]; - public unionFields: UnionField[]; - public interfaceFields: InterfaceField[]; - public interfaces: NamedTypeNode[]; - public objectFields: ObjectField[]; - public nodeDirective?: NodeDirective; - public limit?: LimitDirective; - public singular: string; - public plural: string; - public isGlobalNode: boolean | undefined; - private _idField: string | undefined; - private _idFieldIsInt?: boolean; - - constructor(input: NodeConstructor) { - super(input); - this.relationFields = input.relationFields; - this.connectionFields = input.connectionFields; - this.cypherFields = input.cypherFields; - this.otherDirectives = input.otherDirectives; - this.propagatedDirectives = input.propagatedDirectives; - this.unionFields = input.unionFields; - this.interfaceFields = input.interfaceFields; - this.interfaces = input.interfaces; - this.objectFields = input.objectFields; - this.nodeDirective = input.nodeDirective; - this.limit = input.limitDirective; - this.isGlobalNode = input.isGlobalNode; - this._idField = input.globalIdField; - this._idFieldIsInt = input.globalIdFieldIsInt; - this.singular = this.generateSingular(); - this.plural = this.generatePlural(input.plural); - } - - // Fields you can set in a create or update mutation - public get mutableFields(): MutableField[] { - return [ - ...this.temporalFields, - ...this.enumFields, - ...this.objectFields, - ...this.scalarFields, // these are just custom scalars - ...this.primitiveFields, // these are instead built-in scalars - ...this.interfaceFields, - ...this.objectFields, - ...this.unionFields, - ...this.pointFields, - ]; - } - - /** Fields you can apply auth allow and bind to */ - // Maybe we can remove this as they may not be used anymore in the new auth system - public get authableFields(): MutableField[] { - return [ - ...this.primitiveFields, - ...this.scalarFields, - ...this.enumFields, - ...this.unionFields, - ...this.objectFields, - ...this.temporalFields, - ...this.pointFields, - ...this.cypherFields, - ]; - } - - private get pascalCaseSingular(): string { - return upperFirst(this.singular); - } - - private get pascalCasePlural(): string { - return upperFirst(this.plural); - } - - public get rootTypeFieldNames(): RootTypeFieldNames { - const pascalCasePlural = this.pascalCasePlural; - - return { - create: `create${pascalCasePlural}`, - read: this.plural, - update: `update${pascalCasePlural}`, - delete: `delete${pascalCasePlural}`, - aggregate: `${this.plural}Aggregate`, - subscribe: { - created: `${this.singular}Created`, - updated: `${this.singular}Updated`, - deleted: `${this.singular}Deleted`, - }, - }; - } - - public get aggregateTypeNames(): AggregateTypeNames { - return { - selection: `${this.name}AggregateSelection`, - input: `${this.name}AggregateSelectionInput`, - }; - } - - public get mutationResponseTypeNames(): MutationResponseTypeNames { - const pascalCasePlural = this.pascalCasePlural; - - return { - create: `Create${pascalCasePlural}MutationResponse`, - update: `Update${pascalCasePlural}MutationResponse`, - }; - } - - public get subscriptionEventTypeNames(): SubscriptionEvents { - const pascalCaseSingular = this.pascalCaseSingular; - - return { - create: `${pascalCaseSingular}CreatedEvent`, - update: `${pascalCaseSingular}UpdatedEvent`, - delete: `${pascalCaseSingular}DeletedEvent`, - create_relationship: `${pascalCaseSingular}RelationshipCreatedEvent`, - delete_relationship: `${pascalCaseSingular}RelationshipDeletedEvent`, - }; - } - - public get subscriptionEventPayloadFieldNames(): SubscriptionEvents { - const pascalCaseSingular = this.pascalCaseSingular; - - return { - create: `created${pascalCaseSingular}`, - update: `updated${pascalCaseSingular}`, - delete: `deleted${pascalCaseSingular}`, - create_relationship: `${this.singular}`, - delete_relationship: `${this.singular}`, - }; - } - - public getLabelString(context: Neo4jGraphQLContext): string { - return this.nodeDirective?.getLabelsString(this.name, context) || `:${this.name}`; - } - /** - * Returns the list containing labels mapped with the values contained in the Context. - * Be careful when using this method, labels returned are unescaped. - **/ - public getLabels(context: Neo4jGraphQLContext): string[] { - return this.nodeDirective?.getLabels(this.name, context) || [this.name]; - } - - public getMainLabel(): string { - return this.nodeDirective?.labels?.[0] || this.name; - } - - public getAllLabels(): string[] { - return this.nodeDirective?.labels.length ? this.nodeDirective.labels : [this.name]; - } - - public getGlobalIdField(): string { - if (!this.isGlobalNode || !this._idField) { - throw new Error( - "The 'global' property needs to be set to true on an @id directive before accessing the unique node id field" - ); - } - return this._idField; - } - - public toGlobalId(id: string): string { - const typeName = this.name; - const field = this.getGlobalIdField(); - return toGlobalId({ typeName, field, id }); - } - - public fromGlobalId(relayId: string): DecodedGlobalId { - return fromGlobalId(relayId, this._idFieldIsInt); - } - - private generateSingular(): string { - const singular = camelcase(this.name); - - return `${leadingUnderscores(this.name)}${singular}`; - } - - private generatePlural(inputPlural: string | undefined): string { - const name = inputPlural || this.plural || this.name; - const plural = inputPlural || this.plural ? camelcase(name) : pluralize(camelcase(name)); - - return `${leadingUnderscores(name)}${plural}`; - } -} - -export default Node; diff --git a/packages/graphql/src/classes/NodeDirective.test.ts b/packages/graphql/src/classes/NodeDirective.test.ts deleted file mode 100644 index ed11645834..0000000000 --- a/packages/graphql/src/classes/NodeDirective.test.ts +++ /dev/null @@ -1,111 +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({ labels: ["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({ labels: ["MyLabel", "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({ labels: ["MyOtherLabel", "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().instance(); - const instance = new NodeDirective({ - labels: ["label", "$context.escapeTest1", "$context.escapeTest2"], - }); - const labelString = instance.getLabelsString("label", { - ...context, - escapeTest1: "123-321", - escapeTest2: "He`l`lo", - } as Record); - 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({ - labels: ["label", "$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({ - labels: ["label", "$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({ - labels: ["label", "$context.var1"], - }); - expect(() => { - instance.getLabelsString("label", context); - }).toThrow("Label value not found in context."); - }); - - test("should map labels from cypherParams", () => { - const context = new ContextBuilder({ cypherParams: { tenant: "BULK" } }).instance(); - const instance = new NodeDirective({ - labels: ["label", "$tenant"], - }); - const labelString = instance.getLabelsString("label", context); - expect(labelString).toBe(":label:BULK"); - }); -}); diff --git a/packages/graphql/src/classes/NodeDirective.ts b/packages/graphql/src/classes/NodeDirective.ts deleted file mode 100644 index 265371b88f..0000000000 --- a/packages/graphql/src/classes/NodeDirective.ts +++ /dev/null @@ -1,51 +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 { Neo4jGraphQLError } from "./Error"; -import Cypher from "@neo4j/cypher-builder"; -import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; -import { mapLabelsWithContext } from "../schema-model/utils/map-labels-with-context"; - -export interface NodeDirectiveConstructor { - labels?: string[]; -} - -export class NodeDirective { - public readonly labels: string[]; - - constructor(input: NodeDirectiveConstructor) { - this.labels = input.labels || []; - } - - public getLabelsString(typeName: string, context: Neo4jGraphQLContext): string { - if (!typeName) { - throw new Neo4jGraphQLError("Could not generate label string in @node directive due to empty typeName"); - } - const labels = this.getLabels(typeName, context).map((label) => Cypher.utils.escapeLabel(label)); - return `:${labels.join(":")}`; - } - /** - * Returns the list containing labels mapped with the values contained in the Context. - * Be careful when using this method, labels returned are unescaped. - **/ - public getLabels(typeName: string, context: Neo4jGraphQLContext): string[] { - const labels = !this.labels.length ? [typeName] : this.labels; - return mapLabelsWithContext(labels, context); - } -} diff --git a/packages/graphql/src/classes/Relationship.ts b/packages/graphql/src/classes/Relationship.ts deleted file mode 100644 index 8a7afba549..0000000000 --- a/packages/graphql/src/classes/Relationship.ts +++ /dev/null @@ -1,85 +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 { - CustomEnumField, - CustomResolverField, - CustomScalarField, - CypherField, - PointField, - PrimitiveField, - TemporalField, -} from "../types"; -import { GraphElement } from "./GraphElement"; -import type { MutableField } from "./Node"; - -interface RelationshipConstructor { - name: string; - type?: string; - source: string; // temporary addition to infer the source using the schema model - target: string; // temporary addition to infer the target using the schema model - relationshipFieldName: string; // temporary addition to infer the fieldName using the schema model - description?: string; - properties?: string; - cypherFields?: CypherField[]; - primitiveFields?: PrimitiveField[]; - scalarFields?: CustomScalarField[]; - enumFields?: CustomEnumField[]; - temporalFields?: TemporalField[]; - pointFields?: PointField[]; - customResolverFields?: CustomResolverField[]; -} - -class Relationship extends GraphElement { - public properties?: string; - public source: string; - public target: string; - public relationshipFieldName: string; - - constructor(input: RelationshipConstructor) { - super({ - name: input.name, - description: input.description, - cypherFields: input.cypherFields || [], - primitiveFields: input.primitiveFields || [], - scalarFields: input.scalarFields || [], - enumFields: input.enumFields || [], - temporalFields: input.temporalFields || [], - pointFields: input.pointFields || [], - customResolverFields: input.customResolverFields || [], - }); - - this.properties = input.properties; - this.source = input.source; - this.target = input.target; - this.relationshipFieldName = input.relationshipFieldName; - } - // Fields you can set in a create or update mutation - public get mutableFields(): MutableField[] { - return [ - ...this.temporalFields, - ...this.enumFields, - ...this.scalarFields, // these are just custom scalars - ...this.primitiveFields, // these are instead built-in scalars - ...this.pointFields, - ]; - } -} - -export default Relationship; diff --git a/packages/graphql/src/classes/index.ts b/packages/graphql/src/classes/index.ts index e1f4ee462e..48cf1391f2 100644 --- a/packages/graphql/src/classes/index.ts +++ b/packages/graphql/src/classes/index.ts @@ -20,9 +20,6 @@ import { fieldExtensionsEstimator, simpleEstimator } from "graphql-query-complexity"; export * from "./Error"; -export { GraphElement } from "./GraphElement"; export { Neo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; export { default as Neo4jGraphQL, Neo4jGraphQLConstructor } from "./Neo4jGraphQL"; -export { default as Node, NodeConstructor } from "./Node"; -export { default as Relationship } from "./Relationship"; export const DefaultComplexityEstimators = [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })]; diff --git a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts index 2f32b87267..f4b3b76529 100644 --- a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts @@ -20,7 +20,7 @@ import type { DocumentNode, FieldDefinitionNode } from "graphql"; import { parse } from "graphql"; import type { ResolveTree } from "graphql-parse-resolve-info"; -import { selectionSetToResolveTree } from "../../schema/get-custom-resolver-meta"; +import { selectionSetToResolveTree } from "../../schema/selection-set-to-resolve-tree"; import { getDefinitionCollection } from "../parser/definition-collection"; import type { Annotation } from "./Annotation"; diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 8a04e9e5aa..bfc3d4989c 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -52,8 +52,12 @@ import { findDirective } from "./parser/utils"; import type { NestedOperation, QueryDirection, RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; import { RelationshipDeclaration } from "./relationship/RelationshipDeclaration"; +import type { IResolvers } from "@graphql-tools/utils"; -export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { +export function generateModel( + document: DocumentNode, + userDefinedCustomResolvers?: IResolvers | IResolvers[] +): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { @@ -65,7 +69,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { hydrateInterfacesToTypeNamesMap(definitionCollection); const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => - generateConcreteEntity(node, definitionCollection) + generateConcreteEntity(node, definitionCollection, userDefinedCustomResolvers) ); const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { @@ -517,14 +521,15 @@ function generateRelationshipDeclaration( function generateConcreteEntity( definition: ObjectTypeDefinitionNode, - definitionCollection: DefinitionCollection + definitionCollection: DefinitionCollection, + userDefinedCustomResolvers?: IResolvers | IResolvers[] ): ConcreteEntity { const fields = (definition.fields || []).map((fieldDefinition) => { const isRelationshipAttribute = findDirective(fieldDefinition.directives, relationshipDirective.name); if (isRelationshipAttribute) { return; } - return parseAttribute(fieldDefinition, definitionCollection, definition.fields); + return parseAttribute(fieldDefinition, definitionCollection, definition.fields, userDefinedCustomResolvers); }); // schema configuration directives are propagated onto concrete entities diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 9eb4029168..afe4881db6 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -44,6 +44,7 @@ import type { DefinitionCollection } from "./definition-collection"; import { parseAnnotations } from "./parse-annotation"; import { parseArguments } from "./parse-arguments"; import { findDirective } from "./utils"; +import type { IResolvers } from "@graphql-tools/utils"; export function parseAttributeArguments( fieldArgs: readonly InputValueDefinitionNode[], @@ -62,7 +63,8 @@ export function parseAttributeArguments( export function parseAttribute( field: FieldDefinitionNode, definitionCollection: DefinitionCollection, - definitionFields?: ReadonlyArray + definitionFields?: ReadonlyArray, + userDefinedCustomResolvers?: IResolvers | IResolvers[] ): Attribute { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); @@ -70,6 +72,11 @@ export function parseAttribute( const annotations = parseAnnotations(field.directives || []); annotations.customResolver?.parseRequire(definitionCollection.document, definitionFields); + if (annotations.customResolver) { + if (!userDefinedCustomResolvers?.[name]) { + console.warn(`Custom resolver for ${name} has not been provided`); + } + } const databaseName = getDatabaseName(field); return new Attribute({ diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index 7c02005897..263fbe6d58 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -138,10 +138,8 @@ export class RelationshipAdapter { public findAttribute(name: string): AttributeAdapter | undefined { return this.attributes.get(name); } + /** - * translation-only - * - * @param directed the direction asked during the query, for instance "friends(directed: true)" * @returns the direction to use in the CypherBuilder **/ public getCypherDirection(): "left" | "right" | "undirected" { diff --git a/packages/graphql/src/schema/create-connection-fields.ts b/packages/graphql/src/schema/create-connection-fields.ts deleted file mode 100644 index a9ceaacf64..0000000000 --- a/packages/graphql/src/schema/create-connection-fields.ts +++ /dev/null @@ -1,74 +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 { Relationship } from "../classes"; -import { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { RelationshipDeclarationAdapter } from "../schema-model/relationship/model-adapters/RelationshipDeclarationAdapter"; -import type { ObjectFields } from "./get-obj-field-meta"; - -/** - * TODO [translation-layer-compatibility] - * this file only contains old Relationship class construction - * safe to delete when no longer needed - */ -export function createConnectionFields({ - entityAdapter, - relationshipFields, -}: { - entityAdapter: ConcreteEntityAdapter | InterfaceEntityAdapter; - relationshipFields: Map; -}): Relationship[] { - const relationships: Relationship[] = []; - - const entityRelationships = - entityAdapter instanceof ConcreteEntityAdapter - ? entityAdapter.relationships - : entityAdapter.relationshipDeclarations; - - entityRelationships.forEach((relationship: RelationshipAdapter | RelationshipDeclarationAdapter) => { - const relFields: ObjectFields | undefined = - relationship instanceof RelationshipAdapter && relationship.propertiesTypeName - ? relationshipFields.get(relationship.propertiesTypeName) - : undefined; - - const r = new Relationship({ - name: relationship.operations.relationshipFieldTypename, - type: relationship instanceof RelationshipAdapter ? relationship.type : undefined, - source: relationship.source.name, - target: relationship.target.name, - properties: relationship instanceof RelationshipAdapter ? relationship.propertiesTypeName : undefined, - relationshipFieldName: relationship.name, - ...(relFields - ? { - temporalFields: relFields.temporalFields, - scalarFields: relFields.scalarFields, - primitiveFields: relFields.primitiveFields, - enumFields: relFields.enumFields, - pointFields: relFields.pointFields, - customResolverFields: relFields.customResolverFields, - } - : {}), - }); - relationships.push(r); - }); - - return relationships; -} diff --git a/packages/graphql/src/schema/create-global-nodes.ts b/packages/graphql/src/schema/create-global-nodes.ts index b2bbe1381d..c3a8d15355 100644 --- a/packages/graphql/src/schema/create-global-nodes.ts +++ b/packages/graphql/src/schema/create-global-nodes.ts @@ -20,22 +20,16 @@ import type { GraphQLResolveInfo } from "graphql"; import type { ObjectTypeComposerFieldConfigAsObjectDefinition, SchemaComposer } from "graphql-compose"; import { nodeDefinitions } from "graphql-relay"; -import type { Node } from "../types"; import { globalNodeResolver } from "./resolvers/query/global-node"; import type { Neo4jGraphQLComposedContext } from "./resolvers/composition/wrap-query-and-mutation"; import type { ConcreteEntity } from "../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; // returns true if globalNodeFields added or false if not -export function addGlobalNodeFields( - nodes: Node[], - composer: SchemaComposer, - concreteEntities: ConcreteEntity[] -): boolean { - const globalNodes = nodes.filter((n) => n.isGlobalNode); +export function addGlobalNodeFields(composer: SchemaComposer, concreteEntities: ConcreteEntity[]): boolean { const globalEntities = concreteEntities.map((e) => new ConcreteEntityAdapter(e)).filter((e) => e.isGlobalNode()); - if (globalNodes.length === 0) return false; + if (globalEntities.length === 0) return false; const fetchById = (id: string, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) => { const resolver = globalNodeResolver({ entities: globalEntities }); diff --git a/packages/graphql/src/schema/get-custom-resolver-meta.test.ts b/packages/graphql/src/schema/get-custom-resolver-meta.test.ts deleted file mode 100644 index 405baedf47..0000000000 --- a/packages/graphql/src/schema/get-custom-resolver-meta.test.ts +++ /dev/null @@ -1,872 +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 { - FieldDefinitionNode, - InterfaceTypeDefinitionNode, - ObjectTypeDefinitionNode, - UnionTypeDefinitionNode, -} from "graphql"; -import { Kind } from "graphql"; -import { generateResolveTree } from "../translate/utils/resolveTree"; -import { getCustomResolverMeta, INVALID_SELECTION_SET_ERROR } from "./get-custom-resolver-meta"; - -describe("getCustomResolverMeta", () => { - const authorType = "Author"; - const bookType = "Book"; - const journalType = "Journal"; - const publicationInterface = "Publication"; - - const customResolverField = "publicationsWithAuthor"; - - const objects: ObjectTypeDefinitionNode[] = [ - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: authorType, - }, - interfaces: [], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "name", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publications", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "OUT", - }, - }, - ], - }, - ], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: customResolverField, - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "customResolver", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "requires", - }, - value: { - kind: Kind.STRING, - value: "name publications { publicationYear ...on Book { title } ... on Journal { subject } }", - block: false, - }, - }, - ], - }, - ], - }, - ], - }, - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: bookType, - }, - interfaces: [ - { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - ], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "title", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "author", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: authorType, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "IN", - }, - }, - ], - }, - ], - }, - ], - }, - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: journalType, - }, - interfaces: [ - { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - ], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "subject", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "author", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: authorType, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "IN", - }, - }, - ], - }, - ], - }, - ], - }, - ]; - - const interfaces: InterfaceTypeDefinitionNode[] = [ - { - kind: Kind.INTERFACE_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - interfaces: [], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - ], - }, - ]; - - const unions: UnionTypeDefinitionNode[] = []; - - const object = objects.find((obj) => obj.name.value === authorType) as ObjectTypeDefinitionNode; - - const resolvers = { - [customResolverField]: () => 25, - }; - - let warn: jest.SpyInstance; - - beforeEach(() => { - warn = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); - - afterEach(() => { - warn.mockReset(); - }); - - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toBeUndefined(); - }); - test("should return no required fields if no requires argument", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: {}, - }); - }); - test("should return the correct meta when a list of required fields is provided", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: generateResolveTree({ name: requiredFields }), - }); - }); - - test("should return the correct meta when a selection set of required fields provided", () => { - const requiredFields = `name publications { publicationYear ...on ${bookType} { title } ... on ${journalType} { subject } }`; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: { - name: { - name: "name", - alias: "name", - args: {}, - fieldsByTypeName: {}, - }, - publications: { - name: "publications", - alias: "publications", - args: {}, - fieldsByTypeName: { - Publication: { - publicationYear: { - name: "publicationYear", - alias: "publicationYear", - args: {}, - fieldsByTypeName: {}, - }, - }, - Book: { - title: { - name: "title", - alias: "title", - args: {}, - fieldsByTypeName: {}, - }, - }, - Journal: { - subject: { - name: "subject", - alias: "subject", - args: {}, - fieldsByTypeName: {}, - }, - }, - }, - }, - }, - }); - }); - - test("should throw an error if a non-existant field is passed to the required selection set", () => { - const requiredFields = `name publications doesNotExist { publicationYear ...on ${bookType} { title } ... on ${journalType} { subject } }`; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - expect(() => - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }) - ).toThrow(INVALID_SELECTION_SET_ERROR); - }); - - test("Check throws error if customResolver is not provided", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const resolvers = {}; - - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).toHaveBeenCalledWith(`Custom resolver for ${customResolverField} has not been provided`); - }); - test("Check throws error if customResolver defined on interface", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const resolvers = { - [publicationInterface]: { - [customResolverField]: () => "Hello World!", - }, - }; - - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).toHaveBeenCalledWith(`Custom resolver for ${customResolverField} has not been provided`); - }); -}); diff --git a/packages/graphql/src/schema/get-cypher-meta.test.ts b/packages/graphql/src/schema/get-cypher-meta.test.ts deleted file mode 100644 index 9f93ff06d9..0000000000 --- a/packages/graphql/src/schema/get-cypher-meta.test.ts +++ /dev/null @@ -1,157 +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 { FieldDefinitionNode } from "graphql"; -import { getCypherMeta } from "./get-cypher-meta"; - -describe("getCypherMeta", () => { - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getCypherMeta(field); - - expect(result).toBeUndefined(); - }); - - test("should throw statement required", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "cypher", arguments: [] }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getCypherMeta(field)).toThrow("@cypher statement required"); - }); - - test("should throw statement not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "cypher", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "statement" }, - // @ts-ignore - value: { kind: "NOT A STRING!" }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getCypherMeta(field)).toThrow("@cypher statement is not a string"); - }); - - test("should return the correct meta", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "cypher", - }, - arguments: [ - { - // @ts-ignore - name: { value: "statement" }, - // @ts-ignore - value: { kind: "StringValue", value: "MATCH (m:Movie) RETURN m" }, - }, - { - // @ts-ignore - name: { value: "columnName" }, - // @ts-ignore - value: { kind: "StringValue", value: "m" }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getCypherMeta(field); - - expect(result).toMatchObject({ - statement: "MATCH (m:Movie) RETURN m", - }); - }); -}); diff --git a/packages/graphql/src/schema/get-cypher-meta.ts b/packages/graphql/src/schema/get-cypher-meta.ts deleted file mode 100644 index ebc327a164..0000000000 --- a/packages/graphql/src/schema/get-cypher-meta.ts +++ /dev/null @@ -1,66 +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 { Kind, type DirectiveNode, type FieldDefinitionNode } from "graphql"; - -type CypherMeta = { - statement: string; - columnName: string; -}; - -export function getCypherMeta( - field: FieldDefinitionNode, - interfaceField?: FieldDefinitionNode -): CypherMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "cypher") || - interfaceField?.directives?.find((x) => x.name.value === "cypher"); - if (!directive) { - return undefined; - } - - return { - statement: parseStatementFlag(directive), - columnName: parseColumnNameFlag(directive), - }; -} - -function parseStatementFlag(directive: DirectiveNode): string { - const stmtArg = directive.arguments?.find((x) => x.name.value === "statement"); - if (!stmtArg) { - throw new Error("@cypher statement required"); - } - if (stmtArg.value.kind !== Kind.STRING) { - throw new Error("@cypher statement is not a string"); - } - - return stmtArg.value.value; -} - -function parseColumnNameFlag(directive: DirectiveNode): string { - const stmtArg = directive.arguments?.find((x) => x.name.value === "columnName"); - if (!stmtArg) { - throw new Error("@cypher columnName required"); - } - if (stmtArg.value.kind !== Kind.STRING) { - throw new Error("@cypher columnName is not a string"); - } - - return stmtArg.value.value; -} diff --git a/packages/graphql/src/schema/get-nodes.ts b/packages/graphql/src/schema/get-nodes.ts deleted file mode 100644 index 93fe13276e..0000000000 --- a/packages/graphql/src/schema/get-nodes.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 type { IResolvers } from "@graphql-tools/utils"; -import type { NamedTypeNode } from "graphql"; -import { Node } from "../classes"; -import type { LimitDirective } from "../classes/LimitDirective"; -import type { NodeDirective } from "../classes/NodeDirective"; -import type { DefinitionCollection } from "../schema-model/parser/definition-collection"; -import type { Neo4jGraphQLCallbacks } from "../types"; -import { asArray } from "../utils/utils"; -import { getObjFieldMeta } from "./get-obj-field-meta"; -import parseNodeDirective from "./parse-node-directive"; -import { parseLimitDirective } from "./parse/parse-limit-directive"; -import parsePluralDirective from "./parse/parse-plural-directive"; - -type Nodes = { - nodes: Node[]; - pointInTypeDefs: boolean; - cartesianPointInTypeDefs: boolean; - floatWhereInTypeDefs: boolean; - relationshipPropertyInterfaceNames: Set; - interfaceRelationshipNames: Set; -}; - -function getNodes( - definitionCollection: DefinitionCollection, - options: { - callbacks?: Neo4jGraphQLCallbacks; - userCustomResolvers?: IResolvers | Array; - } -): Nodes { - let pointInTypeDefs = false; - let cartesianPointInTypeDefs = false; - const floatWhereInTypeDefs = false; - - const relationshipPropertyInterfaceNames = new Set(); - const interfaceRelationshipNames = new Set(); - - const nodes: Node[] = []; - for (const definition of definitionCollection.nodes.values()) { - const otherDirectives = (definition.directives || []).filter( - (x) => - ![ - "authorization", - "authentication", - "exclude", - "node", - "fulltext", - "limit", - "plural", - "shareable", - "subscriptionsAuthorization", - "deprecated", - "query", - "mutation", - "subscription", - "jwt", - ].includes(x.name.value) - ); - const propagatedDirectives = (definition.directives || []).filter((x) => - ["deprecated", "shareable"].includes(x.name.value) - ); - - const nodeDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "node"); - const pluralDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "plural"); - const limitDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "limit"); - const nodeInterfaces = [...(definition.interfaces || [])] as NamedTypeNode[]; - - let nodeDirective: NodeDirective; - if (nodeDirectiveDefinition) { - nodeDirective = parseNodeDirective(nodeDirectiveDefinition); - } - - const userCustomResolvers = asArray(options.userCustomResolvers); - const customResolvers = userCustomResolvers.find((r) => !!r[definition.name.value])?.[ - definition.name.value - ] as IResolvers; - - const nodeFields = getObjFieldMeta({ - obj: definition, - definitionCollection, - interfaces: [...definitionCollection.interfaceTypes.values()], - callbacks: options.callbacks, - customResolvers, - }); - - let limitDirective: LimitDirective | undefined; - if (limitDirectiveDefinition) { - limitDirective = parseLimitDirective({ - directive: limitDirectiveDefinition, - definition, - }); - } - - nodeFields.relationFields.forEach((relationship) => { - if (relationship.properties) { - relationshipPropertyInterfaceNames.add(relationship.properties); - } - if (relationship.interface) { - interfaceRelationshipNames.add(relationship.typeMeta.name); - } - }); - - if (!pointInTypeDefs) { - pointInTypeDefs = nodeFields.pointFields.some((field) => field.typeMeta.name === "Point"); - } - if (!cartesianPointInTypeDefs) { - cartesianPointInTypeDefs = nodeFields.pointFields.some((field) => field.typeMeta.name === "CartesianPoint"); - } - - const globalIdFields = nodeFields.primitiveFields.filter((field) => field.isGlobalIdField); - - const globalIdField = globalIdFields[0]; - - const node = new Node({ - name: definition.name.value, - interfaces: nodeInterfaces, - otherDirectives, - propagatedDirectives, - ...nodeFields, - // @ts-ignore we can be sure it's defined - nodeDirective, - limitDirective, - description: definition.description?.value, - isGlobalNode: Boolean(globalIdField), - globalIdField: globalIdField?.fieldName, - globalIdFieldIsInt: globalIdField?.typeMeta?.name === "Int", - plural: parsePluralDirective(pluralDirectiveDefinition), - }); - - nodes.push(node); - } - return { - nodes, - pointInTypeDefs, - cartesianPointInTypeDefs, - floatWhereInTypeDefs, - relationshipPropertyInterfaceNames, - interfaceRelationshipNames, - }; -} - -export default getNodes; diff --git a/packages/graphql/src/schema/get-obj-field-meta.ts b/packages/graphql/src/schema/get-obj-field-meta.ts deleted file mode 100644 index 0f1362bf1e..0000000000 --- a/packages/graphql/src/schema/get-obj-field-meta.ts +++ /dev/null @@ -1,544 +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 { IResolvers } from "@graphql-tools/utils"; -import type { - BooleanValueNode, - DirectiveNode, - EnumValueNode, - InterfaceTypeDefinitionNode, - ListValueNode, - NamedTypeNode, - ObjectTypeDefinitionNode, - StringValueNode, -} from "graphql"; -import { Kind } from "graphql"; -import { SCALAR_TYPES, SPATIAL_TYPES, TEMPORAL_SCALAR_TYPES } from "../constants"; -import type { DefinitionCollection } from "../schema-model/parser/definition-collection"; -import { parseArgumentsFromUnknownDirective } from "../schema-model/parser/parse-arguments"; -import { parseValueNode } from "../schema-model/parser/parse-value-node"; -import type { - BaseField, - ConnectionField, - CustomEnumField, - CustomResolverField, - CustomScalarField, - CypherField, - FilterableOptions, - InterfaceField, - Neo4jGraphQLCallbacks, - ObjectField, - PointField, - PrimitiveField, - RelationField, - SelectableOptions, - SettableOptions, - TemporalField, - TimeStampOperations, - UnionField, -} from "../types"; -import { upperFirst } from "../utils/upper-first"; -import getAliasMeta from "./get-alias-meta"; -import { getCustomResolverMeta } from "./get-custom-resolver-meta"; -import { getCypherMeta } from "./get-cypher-meta"; -import getFieldTypeMeta from "./get-field-type-meta"; -import { getPopulatedByMeta } from "./get-populated-by-meta"; -import getRelationshipMeta from "./get-relationship-meta"; - -export interface ObjectFields { - relationFields: RelationField[]; - connectionFields: ConnectionField[]; - primitiveFields: PrimitiveField[]; - cypherFields: CypherField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - unionFields: UnionField[]; - interfaceFields: InterfaceField[]; - objectFields: ObjectField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - customResolverFields: CustomResolverField[]; -} - -export function getObjFieldMeta({ - obj, - definitionCollection, - interfaces, - callbacks, - customResolvers, -}: { - obj: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; - definitionCollection: DefinitionCollection; - interfaces: InterfaceTypeDefinitionNode[]; - callbacks?: Neo4jGraphQLCallbacks; - customResolvers?: IResolvers | Array; -}): ObjectFields { - const objInterfaceNames = [...(obj.interfaces || [])] as NamedTypeNode[]; - const objects = [...definitionCollection.nodes.values()]; - const unions = [...definitionCollection.unionTypes.values()]; - const objInterfaces = interfaces.filter((i) => objInterfaceNames.map((n) => n.name.value).includes(i.name.value)); - - return obj?.fields?.reduce( - (res: ObjectFields, field) => { - const interfaceField = objInterfaces - .find((i) => i.fields?.map((f) => f.name.value).includes(field.name.value)) - ?.fields?.find((f) => f.name.value === field.name.value); - - // Create array of directives for this field. Field directives override interface field directives. - const directives = [ - ...(field?.directives || []), - ...(interfaceField?.directives || []).filter( - (d) => !field.directives?.find((fd) => fd.name.value === d.name.value) - ), - ]; - - if (directives.some((x) => x.name.value === "private")) { - return res; - } - - const relationshipMeta = getRelationshipMeta(field, interfaceField); - const cypherMeta = getCypherMeta(field, interfaceField); - const customResolverMeta = getCustomResolverMeta({ - field, - object: obj, - objects, - interfaces, - unions, - customResolvers, - interfaceField, - }); - const typeMeta = getFieldTypeMeta(field.type); - const idDirective = directives.find((x) => x.name.value === "id"); - const relayIdDirective = directives.find((x) => x.name.value === "relayId"); - const defaultDirective = directives.find((x) => x.name.value === "default"); - const coalesceDirective = directives.find((x) => x.name.value === "coalesce"); - const timestampDirective = directives.find((x) => x.name.value === "timestamp"); - const aliasDirective = directives.find((x) => x.name.value === "alias"); - const populatedByDirective = directives.find((x) => x.name.value === "populatedBy"); - const selectableDirective = directives.find((x) => x.name.value === "selectable"); - const settableDirective = directives.find((x) => x.name.value === "settable"); - const filterableDirective = directives.find((x) => x.name.value === "filterable"); - - const fieldInterface = definitionCollection.interfaceTypes.get(typeMeta.name); - const fieldUnion = definitionCollection.unionTypes.get(typeMeta.name); - const fieldScalar = definitionCollection.scalarTypes.get(typeMeta.name); - const fieldEnum = definitionCollection.enumTypes.get(typeMeta.name); - const fieldObject = definitionCollection.objectTypes.get(typeMeta.name); - const fieldTemporal = TEMPORAL_SCALAR_TYPES.includes(typeMeta.name); - const fieldPoint = SPATIAL_TYPES.includes(typeMeta.name); - - const selectableOptions = parseSelectableDirective(selectableDirective); - const settableOptions = parseSettableDirective(settableDirective); - const filterableOptions = parseFilterableDirective(filterableDirective); - - const baseField: BaseField = { - fieldName: field.name.value, - dbPropertyName: field.name.value, - typeMeta, - selectableOptions, - settableOptions, - filterableOptions, - otherDirectives: (directives || []).filter( - (x) => - ![ - "relationship", - "cypher", - "id", - "authorization", - "authentication", - "customResolver", - "default", - "coalesce", - "timestamp", - "alias", - "callback", - "populatedBy", - "jwtClaim", - "selectable", - "settable", - "subscriptionsAuthorization", - "filterable", - "relayId", - ].includes(x.name.value) - ), - arguments: [...(field.arguments || [])], - description: field.description?.value, - }; - - if (aliasDirective) { - const aliasMeta = getAliasMeta(aliasDirective); - if (aliasMeta) { - baseField.dbPropertyName = aliasMeta.property; - baseField.dbPropertyNameUnescaped = aliasMeta.propertyUnescaped; - } - } - - if (relationshipMeta) { - const relationField: RelationField = { - ...baseField, - ...relationshipMeta, - inherited: false, - }; - - if (fieldUnion) { - const nodes: string[] = []; - - fieldUnion.types?.forEach((type) => { - nodes.push(type.name.value); - }); - - const unionField: UnionField = { - ...baseField, - nodes, - }; - - relationField.union = unionField; - } - - if (fieldInterface) { - const implementations = objects - .filter((n) => n.interfaces?.some((i) => i.name.value === fieldInterface.name.value)) - .map((n) => n.name.value); - - relationField.interface = { - ...baseField, - implementations, - }; - } - - // TODO: This will be brittle if more than one interface - - let connectionPrefix = obj.name.value; - - if (obj.interfaces && obj.interfaces.length) { - const firstInterface = obj.interfaces[0]; - if (!firstInterface) { - throw new Error("Cannot get interface in getObjFieldMeta"); - } - - const inter = interfaces.find( - (i) => i.name.value === firstInterface.name.value - ) as InterfaceTypeDefinitionNode; - - if (inter.fields?.some((f) => f.name.value === baseField.fieldName)) { - connectionPrefix = firstInterface.name.value; - relationField.inherited = true; - } - } - - relationField.connectionPrefix = connectionPrefix; - - const connectionTypeName = `${connectionPrefix}${upperFirst(`${baseField.fieldName}Connection`)}`; - const relationshipTypeName = `${connectionPrefix}${upperFirst(`${baseField.fieldName}Relationship`)}`; - - const connectionField: ConnectionField = { - fieldName: `${baseField.fieldName}Connection`, - relationshipTypeName, - selectableOptions, - settableOptions, - filterableOptions, - typeMeta: { - name: connectionTypeName, - required: true, - pretty: `${connectionTypeName}!`, - input: { - where: { - type: `${connectionTypeName}Where`, - pretty: `${connectionTypeName}Where`, - }, - create: { - type: "", - pretty: "", - }, - update: { - type: "", - pretty: "", - }, - }, - }, - otherDirectives: baseField.otherDirectives, - arguments: [...(field.arguments || [])], - description: field.description?.value, - relationship: relationField, - }; - - res.relationFields.push(relationField); - res.connectionFields.push(connectionField); - } else if (cypherMeta) { - const cypherField: CypherField = { - ...baseField, - ...cypherMeta, - isEnum: !!fieldEnum, - isScalar: !!fieldScalar || SCALAR_TYPES.includes(typeMeta.name), - }; - res.cypherFields.push(cypherField); - } else if (customResolverMeta) { - res.customResolverFields.push({ ...baseField, ...customResolverMeta }); - } else if (fieldScalar) { - const scalarField: CustomScalarField = { - ...baseField, - }; - res.scalarFields.push(scalarField); - } else if (fieldEnum) { - const enumField: CustomEnumField = { - kind: "Enum", - ...baseField, - }; - - if (defaultDirective) { - const defaultValue = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - - if (enumField.typeMeta.array) { - enumField.defaultValue = (defaultValue as ListValueNode).values.map((v) => { - return (v as EnumValueNode).value; - }); - } else { - enumField.defaultValue = (defaultValue as EnumValueNode).value; - } - } - - if (coalesceDirective) { - const coalesceValue = coalesceDirective.arguments?.find((a) => a.name.value === "value")?.value; - - if (enumField.typeMeta.array) { - enumField.coalesceValue = (coalesceValue as ListValueNode).values.map((v) => { - return (v as EnumValueNode).value; - }); - } else { - // TODO: avoid duplication with primitives - enumField.coalesceValue = `"${(coalesceValue as EnumValueNode).value}"`; - } - } - - res.enumFields.push(enumField); - } else if (fieldUnion) { - const unionField: UnionField = { - ...baseField, - }; - res.unionFields.push(unionField); - } else if (fieldInterface) { - res.interfaceFields.push({ - ...baseField, - }); - } else if (fieldObject) { - const objectField: ObjectField = { - ...baseField, - }; - res.objectFields.push(objectField); - } else { - if (fieldTemporal) { - const temporalField: TemporalField = { - ...baseField, - }; - - if (populatedByDirective) { - const callback = getPopulatedByMeta(populatedByDirective, callbacks); - temporalField.callback = callback; - } - - if (timestampDirective) { - const operations = timestampDirective?.arguments?.find((x) => x.name.value === "operations") - ?.value as ListValueNode; - - const timestamps = operations - ? (operations?.values.map((x) => parseValueNode(x)) as TimeStampOperations[]) - : (["CREATE", "UPDATE"] as TimeStampOperations[]); - - temporalField.timestamps = timestamps; - } - - if (defaultDirective) { - const value = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - temporalField.defaultValue = (value as StringValueNode).value; - } - - res.temporalFields.push(temporalField); - } else if (fieldPoint) { - const pointField: PointField = { - ...baseField, - }; - res.pointFields.push(pointField); - } else { - const primitiveField: PrimitiveField = { - ...baseField, - }; - - if (populatedByDirective) { - const callback = getPopulatedByMeta(populatedByDirective, callbacks); - primitiveField.callback = callback; - } - - if (idDirective) { - // TODO: remove the following as the argument "autogenerate" does not exists anymore - const autogenerate = idDirective.arguments?.find((a) => a.name.value === "autogenerate"); - if (!autogenerate || (autogenerate.value as BooleanValueNode).value) { - primitiveField.autogenerate = true; - } - } - - if (relayIdDirective) { - primitiveField.isGlobalIdField = true; - } - - if (defaultDirective) { - const value = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - - const typeError = `Default value for ${obj.name.value}.${primitiveField.fieldName} does not have matching type ${primitiveField.typeMeta.name}`; - - switch (baseField.typeMeta.name) { - case "ID": - case "String": - if (value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.defaultValue = value.value; - break; - case "Boolean": - if (value?.kind !== Kind.BOOLEAN) { - throw new Error(typeError); - } - primitiveField.defaultValue = value.value; - break; - case "Int": - if (value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseInt(value.value, 10); - break; - case "BigInt": - if (value?.kind !== Kind.INT && value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseInt(value.value, 10); - break; - case "Float": - if (value?.kind !== Kind.FLOAT && value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseFloat(value.value); - break; - default: - throw new Error( - "@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime." - ); - } - } - - if (coalesceDirective) { - const value = coalesceDirective.arguments?.find((a) => a.name.value === "value")?.value; - - const typeError = `coalesce() value for ${obj.name.value}.${primitiveField.fieldName} does not have matching type ${primitiveField.typeMeta.name}`; - - switch (baseField.typeMeta.name) { - case "ID": - case "String": - if (value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.coalesceValue = `"${value.value}"`; - break; - case "Boolean": - if (value?.kind !== Kind.BOOLEAN) { - throw new Error(typeError); - } - primitiveField.coalesceValue = value.value; - break; - case "Int": - if (value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.coalesceValue = parseInt(value.value, 10); - break; - case "Float": - if (value?.kind !== Kind.FLOAT) { - throw new Error(typeError); - } - primitiveField.coalesceValue = parseFloat(value.value); - break; - default: - throw new Error( - "@coalesce directive can only be used on types: Int | Float | String | Boolean | ID | DateTime" - ); - } - } - - res.primitiveFields.push(primitiveField); - } - } - - return res; - }, - { - relationFields: [], - connectionFields: [], - primitiveFields: [], - cypherFields: [], - scalarFields: [], - enumFields: [], - unionFields: [], - interfaceFields: [], - objectFields: [], - temporalFields: [], - pointFields: [], - customResolverFields: [], - } - ) as ObjectFields; -} - -function parseSelectableDirective(directive: DirectiveNode | undefined): SelectableOptions { - const defaultArguments = { - onRead: true, - onAggregate: true, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - onRead: args.onRead ?? defaultArguments.onRead, - onAggregate: args.onAggregate ?? defaultArguments.onAggregate, - }; -} - -function parseSettableDirective(directive: DirectiveNode | undefined): SettableOptions { - const defaultArguments = { - onCreate: true, - onUpdate: true, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - onCreate: args.onCreate ?? defaultArguments.onCreate, - onUpdate: args.onUpdate ?? defaultArguments.onUpdate, - }; -} - -function parseFilterableDirective(directive: DirectiveNode | undefined): FilterableOptions { - const defaultArguments = { - byValue: true, - byAggregate: directive === undefined ? true : false, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - byValue: args.byValue ?? defaultArguments.byValue, - byAggregate: args.byAggregate ?? defaultArguments.byAggregate, - }; -} diff --git a/packages/graphql/src/schema/get-populated-by-meta.ts b/packages/graphql/src/schema/get-populated-by-meta.ts deleted file mode 100644 index db95373c39..0000000000 --- a/packages/graphql/src/schema/get-populated-by-meta.ts +++ /dev/null @@ -1,39 +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, ArgumentNode, ListValueNode, StringValueNode } from "graphql"; -import type { Callback, CallbackOperations, Neo4jGraphQLCallbacks } from "../types"; - -export function getPopulatedByMeta(directive: DirectiveNode, callbacks?: Neo4jGraphQLCallbacks): Callback { - const operationsArg = directive.arguments?.find((x) => x.name.value === "operations") as ArgumentNode; - const callbackArg = directive.arguments?.find((x) => x.name.value === "callback") as ArgumentNode; - - const operationsList = operationsArg.value as ListValueNode; - const operations = operationsList.values.map((value) => (value as StringValueNode).value) as CallbackOperations[]; - const callbackName = (callbackArg?.value as StringValueNode)?.value; - - if (typeof (callbacks || {})[callbackName] !== "function") { - throw new Error(`PopulatedBy callback '${callbackName}' must be of type function`); - } - - return { - operations, - callbackName, - }; -} diff --git a/packages/graphql/src/schema/get-relationship-meta.test.ts b/packages/graphql/src/schema/get-relationship-meta.test.ts deleted file mode 100644 index d6234a1f73..0000000000 --- a/packages/graphql/src/schema/get-relationship-meta.test.ts +++ /dev/null @@ -1,629 +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 { FieldDefinitionNode } from "graphql"; -import { Kind } from "graphql"; -import getRelationshipMeta from "./get-relationship-meta"; - -describe("getRelationshipMeta", () => { - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toBeUndefined(); - }); - - test("should throw when relationship has no arguments", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "relationship", arguments: [] }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" of required type \\"String!\\" was not provided."` - ); - }); - - test("should throw when direction is missing", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - // @ts-ignore - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" of required type \\"RelationshipDirection!\\" was not provided."` - ); - }); - - test("should throw when direction not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.BOOLEAN, value: true }, - }, - ], - }, - ], - }; - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" has invalid value true."` - ); - }); - - test("should throw when direction is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "INVALID!" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" has invalid value INVALID!."` - ); - }); - - test("should throw when type is missing", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" of required type \\"String!\\" was not provided."` - ); - }); - - test("should throw when type not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - // @ts-ignore - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.FLOAT, value: "1.3" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" has invalid value 1.3."` - ); - }); - - test("should return the correct meta with direction and escaped type", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - // @ts-ignore - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - // @ts-ignore - value: { kind: Kind.STRING, value: "ACTED_IN$" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "`ACTED_IN$`", - direction: "IN", - typeUnescaped: "ACTED_IN$", - }); - }); - - test("should throw when properties is not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.BOOLEAN, value: true }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"properties\\" has invalid value true."` - ); - }); - - test("should return the correct meta with direction, type and properties", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.STRING, value: "ActedIn" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - properties: "ActedIn", - }); - }); - - test("should throw when queryDirection is not an enum", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.STRING, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"queryDirection\\" has invalid value \\"IN\\"."` - ); - }); - - test("should return the correct meta with direction, type and queryDirection", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.ENUM, value: "UNDIRECTED" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - queryDirection: "UNDIRECTED", - }); - }); - - test("should throw when nestedOperations is not a list", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { kind: Kind.STRING, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value \\"IN\\"."` - ); - }); - - test("should throw when nestedOperations is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.STRING, - value: "FAIL", - }, - ], - }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value [\\"FAIL\\"]."` - ); - }); - - test("should throw when nestedOperations value at index position 1 is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "FAIL", - }, - ], - }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value [CONNECT, FAIL]."` - ); - }); - - test("should return the correct meta with direction, type and nestedOperations", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "CREATE", - }, - ], - }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - nestedOperations: ["CONNECT", "CREATE"], - }); - }); - - test("should return the correct meta for all possible arguments", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.STRING, value: "ActedIn" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.ENUM, value: "UNDIRECTED" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "CREATE", - }, - ], - }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - properties: "ActedIn", - queryDirection: "UNDIRECTED", - nestedOperations: ["CONNECT", "CREATE"], - }); - }); -}); diff --git a/packages/graphql/src/schema/get-relationship-meta.ts b/packages/graphql/src/schema/get-relationship-meta.ts deleted file mode 100644 index e7baaa43f0..0000000000 --- a/packages/graphql/src/schema/get-relationship-meta.ts +++ /dev/null @@ -1,63 +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, FieldDefinitionNode } from "graphql"; -import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../constants"; -import { relationshipDirective } from "../graphql/directives/relationship"; -import { parseArguments } from "../schema-model/parser/parse-arguments"; -import Cypher from "@neo4j/cypher-builder"; -import type { RelationshipDirection } from "../schema-model/relationship/Relationship"; - -type RelationshipMeta = { - direction: RelationshipDirection; - type: string; - typeUnescaped: string; - properties?: string; - queryDirection: RelationshipQueryDirectionOption; - nestedOperations: RelationshipNestedOperationsOption[]; - aggregate: boolean; -}; - -function getRelationshipMeta( - field: FieldDefinitionNode, - interfaceField?: FieldDefinitionNode -): RelationshipMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "relationship") || - interfaceField?.directives?.find((x) => x.name.value === "relationship"); - if (!directive) { - return undefined; - } - - const relationshipMetaObject = getRelationshipDirectiveArguments(directive); - const typeUnescaped = relationshipMetaObject.type as string; - const type = Cypher.utils.escapeLabel(typeUnescaped); - - return { - ...relationshipMetaObject, - type, - typeUnescaped, - } as RelationshipMeta; -} - -function getRelationshipDirectiveArguments(directiveNode: DirectiveNode) { - return parseArguments(relationshipDirective, directiveNode); -} - -export default getRelationshipMeta; diff --git a/packages/graphql/src/schema/make-augmented-schema.test.ts b/packages/graphql/src/schema/make-augmented-schema.test.ts index 61f7793628..b5a8be88b3 100644 --- a/packages/graphql/src/schema/make-augmented-schema.test.ts +++ b/packages/graphql/src/schema/make-augmented-schema.test.ts @@ -29,7 +29,6 @@ import { } from "graphql"; import { pluralize } from "graphql-compose"; import { gql } from "graphql-tag"; -import { Node } from "../classes"; import { generateModel } from "../schema-model/generate-model"; import makeAugmentedSchema from "./make-augmented-schema"; import { ComplexityEstimatorHelper } from "../classes/ComplexityEstimatorHelper"; @@ -53,20 +52,17 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; const queryObject = document.definitions.find( (x) => x.kind === Kind.OBJECT_TYPE_DEFINITION && x.name.value === "Query" ) as ObjectTypeDefinitionNode; ["Actor", "Movie"].forEach((type) => { - const node = neoSchema.nodes.find((x) => x.name === type); - expect(node).toBeInstanceOf(Node); - const nodeObject = document.definitions.find( - (x) => x.kind === Kind.OBJECT_TYPE_DEFINITION && x.name.value === type - ); - expect(nodeObject).toBeTruthy(); - // Find const nodeFindQuery = queryObject.fields?.find((x) => x.name.value === pluralize(camelCase(type))); const nodeFindQueryType = ( @@ -97,7 +93,11 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; @@ -271,7 +271,11 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; @@ -291,9 +295,13 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - expect(() => makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) })).not.toThrow( - 'Error: Type with name "ActionMapping" does not exists' - ); + expect(() => + makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }) + ).not.toThrow('Error: Type with name "ActionMapping" does not exists'); }); }); }); diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index e38560ae68..9ef4506be1 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -32,15 +32,11 @@ import type { import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString, Kind, parse, print } from "graphql"; import type { ObjectTypeComposer } from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; -import type { Node } from "../classes"; -import type Relationship from "../classes/Relationship"; import * as Scalars from "../graphql/scalars"; import { AggregationTypesMapper } from "./aggregations/aggregation-types-mapper"; import { augmentFulltextSchema } from "./augment/fulltext"; import { ensureNonEmptyInput } from "./ensure-non-empty-input"; import getCustomResolvers from "./get-custom-resolvers"; -import type { ObjectFields } from "./get-obj-field-meta"; -import { getObjFieldMeta } from "./get-obj-field-meta"; import { createResolver } from "./resolvers/mutation/create"; import { deleteResolver } from "./resolvers/mutation/delete"; import { updateResolver } from "./resolvers/mutation/update"; @@ -68,10 +64,9 @@ import { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/In import { UnionEntityAdapter } from "../schema-model/entity/model-adapters/UnionEntityAdapter"; import { getDefinitionCollection } from "../schema-model/parser/definition-collection"; import { RelationshipDeclarationAdapter } from "../schema-model/relationship/model-adapters/RelationshipDeclarationAdapter"; -import type { CypherField, Neo4jFeaturesSettings } from "../types"; +import type { Neo4jFeaturesSettings } from "../types"; import { asArray, filterTruthy } from "../utils/utils"; import { augmentVectorSchema } from "./augment/vector"; -import { createConnectionFields } from "./create-connection-fields"; import { addGlobalNodeFields } from "./create-global-nodes"; import { createRelationshipFields } from "./create-relationship-fields/create-relationship-fields"; import { AugmentedSchemaGenerator } from "./generation/AugmentedSchemaGenerator"; @@ -82,9 +77,7 @@ import { withObjectType } from "./generation/object-type"; import { withMutationResponseTypes } from "./generation/response-types"; import { withUpdateInputType } from "./generation/update-input"; import { withUniqueWhereInputType, withWhereInputType } from "./generation/where-input"; -import getNodes from "./get-nodes"; import { getResolveAndSubscriptionMethods } from "./get-resolve-and-subscription-methods"; -import { filterInterfaceTypes } from "./make-augmented-schema/filter-interface-types"; import { getUserDefinedDirectives } from "./make-augmented-schema/user-defined-directives"; import { cypherResolver } from "./resolvers/query/cypher"; import { generateSubscriptionTypes } from "./subscriptions/generate-subscription-types"; @@ -96,33 +89,27 @@ function definitionNodeHasName(x: DefinitionNode): x is DefinitionNode & { name: function makeAugmentedSchema({ document, features, - userCustomResolvers, subgraph, schemaModel, complexityEstimatorHelper, }: { document: DocumentNode; features?: Neo4jFeaturesSettings; - userCustomResolvers?: IResolvers | Array; subgraph?: Subgraph; schemaModel: Neo4jGraphQLSchemaModel; complexityEstimatorHelper: ComplexityEstimatorHelper; }): { - nodes: Node[]; - relationships: Relationship[]; typeDefs: DocumentNode; resolvers: IResolvers; } { const composer = new SchemaComposer(); - const callbacks = features?.populatedBy?.callbacks; - let relationships: Relationship[] = []; //TODO: definition collection is being used to harmonize schema generation with schema model, //however make augmented schema inner methods are still accepting arrays as they were defined by the previous getDefinitionNodes const definitionCollection = getDefinitionCollection(document); const { - interfaceTypes, + // interfaceTypes, scalarTypes, userDefinedObjectTypes, enumTypes, @@ -267,14 +254,7 @@ function makeAugmentedSchema({ } const aggregationTypesMapper = new AggregationTypesMapper(composer, subgraph); - - const getNodesResult = getNodes(definitionCollection, { callbacks, userCustomResolvers }); - - const { nodes, interfaceRelationshipNames } = getNodesResult; - - const hasGlobalNodes = addGlobalNodeFields(nodes, composer, schemaModel.concreteEntities); - - const { filteredInterfaceTypes } = filterInterfaceTypes(interfaceTypes.values(), interfaceRelationshipNames); + const hasGlobalNodes = addGlobalNodeFields(composer, schemaModel.concreteEntities); const { userDefinedFieldDirectivesForNode, @@ -284,24 +264,6 @@ function makeAugmentedSchema({ userDefinedDirectivesForUnion, } = getUserDefinedDirectives(definitionCollection); - /** - * TODO [translation-layer-compatibility] - * keeping this `relationshipFields` scaffold for backwards compatibility on translation layer - * actual functional logic is in schemaModel.concreteEntities.forEach - */ - const relationshipFields = new Map(); - for (const relationship of definitionCollection.relationshipProperties.values()) { - const relFields = getObjFieldMeta({ - interfaces: filteredInterfaceTypes, - definitionCollection, - obj: relationship, - callbacks, - }); - - relationshipFields.set(relationship.name.value, relFields); - } - - // this is the new "functional" way for the above forEach // helper to only create relationshipProperties Interface types once, even if multiple relationships reference it const seenRelationshipPropertiesTypes = new Set(); schemaModel.entities.forEach((entity) => { @@ -348,23 +310,9 @@ function makeAugmentedSchema({ complexityEstimatorHelper, schemaModel, }); - const connectionFields = createConnectionFields({ - entityAdapter: interfaceEntityAdapter, - relationshipFields, - }); - relationships = [...relationships, ...connectionFields]; return; } if (entity instanceof ConcreteEntity) { - /** - * TODO [translation-layer-compatibility] - * need the node for fulltext translation - */ - const node = nodes.find((n) => n.name === entity.name); - if (!node) { - throw new Error(`Node not found with the name ${entity.name}`); - } - const concreteEntityAdapter = new ConcreteEntityAdapter(entity); const userDefinedFieldDirectives = userDefinedFieldDirectivesForNode.get(concreteEntityAdapter.name); if (!userDefinedFieldDirectives) { @@ -384,24 +332,17 @@ function makeAugmentedSchema({ userDefinedFieldDirectives, propagatedDirectives, aggregationTypesMapper, - node, seenRelationshipPropertiesTypes, userDefinedDirectivesForNode, userDefinedFieldDirectivesForNode, complexityEstimatorHelper, schemaModel, }); - - const connectionFields = createConnectionFields({ - entityAdapter: concreteEntityAdapter, - relationshipFields, - }); - relationships = [...relationships, ...connectionFields]; return; } }); - if (nodes.length) { + if (schemaModel.concreteEntities.length) { generateSubscriptionTypes({ schemaComposer: composer, schemaModel, @@ -421,19 +362,7 @@ function makeAugmentedSchema({ const userDefinedFieldDirectives = userDefinedFieldDirectivesForNode.get(type) as Map; for (const attributeAdapter of operationAdapter.attributes.values()) { - /** - * TODO [translation-layer-compatibility] - * needed for compatibility with translation layer - */ - const objectFields = getObjFieldMeta({ - obj: customResolvers[`customCypher${type}`], - interfaces: filteredInterfaceTypes, - definitionCollection, - callbacks, - }); - const field = objectFields.cypherFields.find((f) => f.fieldName === attributeAdapter.name) as CypherField; const customResolver = cypherResolver({ - field, attributeAdapter, type: type as "Query" | "Mutation", }); @@ -537,8 +466,6 @@ function makeAugmentedSchema({ }; return { - nodes, - relationships, typeDefs: parsedDoc, resolvers: generatedResolvers, }; @@ -614,7 +541,6 @@ function generateObjectType({ userDefinedObjectDirectives, propagatedDirectives, aggregationTypesMapper, - node, seenRelationshipPropertiesTypes, userDefinedDirectivesForNode, userDefinedFieldDirectivesForNode, @@ -629,7 +555,6 @@ function generateObjectType({ userDefinedObjectDirectives: DirectiveNode[]; propagatedDirectives: DirectiveNode[]; aggregationTypesMapper: AggregationTypesMapper; - node: Node; seenRelationshipPropertiesTypes: Set; userDefinedDirectivesForNode: Map; userDefinedFieldDirectivesForNode: Map>; @@ -716,7 +641,6 @@ function generateObjectType({ if (concreteEntityAdapter.isCreatable) { composer.Mutation.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.create]: createResolver({ - node, concreteEntityAdapter, }), }); @@ -742,7 +666,6 @@ function generateObjectType({ if (concreteEntityAdapter.isUpdatable) { composer.Mutation.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.update]: updateResolver({ - node, concreteEntityAdapter, }), }); diff --git a/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts b/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts deleted file mode 100644 index c75b0fddd0..0000000000 --- a/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts +++ /dev/null @@ -1,42 +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 { InterfaceTypeDefinitionNode } from "graphql"; - -export function filterInterfaceTypes( - interfaceTypes: Iterable, - interfaceRelationshipNames: Set -): { - interfaceRelationships: InterfaceTypeDefinitionNode[]; - filteredInterfaceTypes: InterfaceTypeDefinitionNode[]; -} { - const interfaceRelationships: InterfaceTypeDefinitionNode[] = []; - const filteredInterfaceTypes: InterfaceTypeDefinitionNode[] = []; - for (const interfaceType of interfaceTypes) { - if (interfaceRelationshipNames.has(interfaceType.name.value)) { - interfaceRelationships.push(interfaceType); - } else { - filteredInterfaceTypes.push(interfaceType); - } - } - return { - interfaceRelationships, - filteredInterfaceTypes, - }; -} diff --git a/packages/graphql/src/schema/parse-node-directive.test.ts b/packages/graphql/src/schema/parse-node-directive.test.ts deleted file mode 100644 index 406cf951ef..0000000000 --- a/packages/graphql/src/schema/parse-node-directive.test.ts +++ /dev/null @@ -1,67 +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 labels, type name ignored", () => { - const typeDefs = ` - type TestType @node(labels:["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({ labels: ["Label", "AnotherLabel"] }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); - - test("should return a node directive with labels, type name included", () => { - const typeDefs = ` - type TestType @node(labels:["TestType", "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({ labels: ["TestType", "Label", "AnotherLabel"] }); - - 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 deleted file mode 100644 index deafe053c4..0000000000 --- a/packages/graphql/src/schema/parse-node-directive.ts +++ /dev/null @@ -1,39 +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 } from "graphql"; -import { valueFromASTUntyped } from "graphql"; -import { NodeDirective } from "../classes/NodeDirective"; - -function parseNodeDirective(nodeDirective: DirectiveNode | undefined) { - if (!nodeDirective || nodeDirective.name.value !== "node") { - throw new Error("Undefined or incorrect directive passed into parseNodeDirective function"); - } - - return new NodeDirective({ - labels: getArgumentValue(nodeDirective, "labels"), - }); -} - -function getArgumentValue(directive: DirectiveNode, name: string): T | undefined { - const argument = directive.arguments?.find((a) => a.name.value === name); - return argument ? (valueFromASTUntyped(argument.value) as T) : undefined; -} - -export default parseNodeDirective; diff --git a/packages/graphql/src/schema/parse/parse-limit-directive.test.ts b/packages/graphql/src/schema/parse/parse-limit-directive.test.ts deleted file mode 100644 index 4722ee6d78..0000000000 --- a/packages/graphql/src/schema/parse/parse-limit-directive.test.ts +++ /dev/null @@ -1,144 +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 gql from "graphql-tag"; -import * as neo4j from "neo4j-driver"; -import { LimitDirective } from "../../classes/LimitDirective"; -import { parseLimitDirective } from "./parse-limit-directive"; - -describe("parseLimitDirective", () => { - test("max and default argument", () => { - const maxLimit = 100; - const defaultLimit = 10; - - const typeDefs = gql` - type Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - const result = parseLimitDirective({ - directive, - definition, - }); - - expect(result).toEqual(new LimitDirective({ max: neo4j.int(maxLimit), default: neo4j.int(defaultLimit) })); - }); - - test("should return correct object if default limit is undefined", () => { - const typeDefs = gql` - type Movie @limit @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - const result = parseLimitDirective({ - directive, - definition, - }); - expect(result).toEqual(new LimitDirective({})); - }); - - test("fail if default argument is bigger than max", () => { - const maxLimit = 10; - const defaultLimit = 100; - - const typeDefs = gql` - type Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow( - `Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) invalid default value, 'default' must be smaller than 'max'` - ); - }); - - describe("default argument", () => { - test("should throw error when default limit is less than or equal to 0", () => { - [-10, -100, 0].forEach((i) => { - const typeDefs = gql` - type Movie @limit(default: ${i}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(default: ${i}) invalid value: '${i}', it should be a number greater than 0`); - }); - }); - }); - - describe("max argument", () => { - test("should fail if value is 0", () => { - const typeDefs = gql` - type Movie @limit(max: 0) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(max: 0) invalid value: '0', it should be a number greater than 0`); - }); - - test("should fail if value is less 0", () => { - const typeDefs = gql` - type Movie @limit(max: -10) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(max: -10) invalid value: '-10', it should be a number greater than 0`); - }); - }); -}); diff --git a/packages/graphql/src/schema/parse/parse-limit-directive.ts b/packages/graphql/src/schema/parse/parse-limit-directive.ts deleted file mode 100644 index 66d62c48b7..0000000000 --- a/packages/graphql/src/schema/parse/parse-limit-directive.ts +++ /dev/null @@ -1,82 +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 { ArgumentNode, DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import * as neo4j from "neo4j-driver"; -import { LimitDirective } from "../../classes/LimitDirective"; -import { Neo4jGraphQLError } from "../../classes/Error"; -import { parseValueNode } from "../../schema-model/parser/parse-value-node"; - -export function parseLimitDirective({ - directive, - definition, -}: { - directive: DirectiveNode; - definition: ObjectTypeDefinitionNode; -}): LimitDirective { - const defaultLimitArgument = directive.arguments?.find((argument) => argument.name.value === "default"); - const maxLimitArgument = directive.arguments?.find((argument) => argument.name.value === "max"); - - const defaultLimit = parseArgumentToInt(defaultLimitArgument); - const maxLimit = parseArgumentToInt(maxLimitArgument); - - const limit = { default: defaultLimit, max: maxLimit }; - - const limitError = validateLimitArguments(limit, definition.name.value); - if (limitError) { - throw limitError; - } - - return new LimitDirective(limit); -} - -function parseArgumentToInt(argument: ArgumentNode | undefined): neo4j.Integer | undefined { - if (argument) { - const parsed = parseValueNode(argument.value) as number; - return neo4j.int(parsed); - } - return undefined; -} - -function validateLimitArguments( - arg: { default: neo4j.Integer | undefined; max: neo4j.Integer | undefined }, - typeName: string -): Neo4jGraphQLError | undefined { - const maxLimit = arg.max?.toNumber(); - const defaultLimit = arg.default?.toNumber(); - - if (defaultLimit !== undefined && defaultLimit <= 0) { - return new Neo4jGraphQLError( - `${typeName} @limit(default: ${defaultLimit}) invalid value: '${defaultLimit}', it should be a number greater than 0` - ); - } - if (maxLimit !== undefined && maxLimit <= 0) { - return new Neo4jGraphQLError( - `${typeName} @limit(max: ${maxLimit}) invalid value: '${maxLimit}', it should be a number greater than 0` - ); - } - if (maxLimit && defaultLimit) { - if (maxLimit < defaultLimit) { - return new Neo4jGraphQLError( - `${typeName} @limit(max: ${maxLimit}, default: ${defaultLimit}) invalid default value, 'default' must be smaller than 'max'` - ); - } - } - return undefined; -} diff --git a/packages/graphql/src/schema/parse/parse-plural-directive.test.ts b/packages/graphql/src/schema/parse/parse-plural-directive.test.ts deleted file mode 100644 index fa079cdc45..0000000000 --- a/packages/graphql/src/schema/parse/parse-plural-directive.test.ts +++ /dev/null @@ -1,36 +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 parsePluralDirective from "./parse-plural-directive"; - -describe("parsePluralDirective", () => { - test("Should return the custom plural string", () => { - const plural = "testTypes123"; - const typeDefs = ` - type TestType @plural(value: "${plural}") { - name: String - } - `; - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - expect(parsePluralDirective(directive)).toBe(plural); - }); -}); diff --git a/packages/graphql/src/schema/parse/parse-plural-directive.ts b/packages/graphql/src/schema/parse/parse-plural-directive.ts deleted file mode 100644 index 10e7e6f822..0000000000 --- a/packages/graphql/src/schema/parse/parse-plural-directive.ts +++ /dev/null @@ -1,32 +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, StringValueNode } from "graphql"; - -/** - * Parse the plural directive and return the plural value. - * @param pluralDirective The plural directicve to parse. - * @returns The plural value. - */ -export default function parsePluralDirective(pluralDirective: DirectiveNode | undefined): string | undefined { - return ( - (pluralDirective?.arguments?.find((argument) => argument.name.value === "value")?.value as StringValueNode) - ?.value || undefined - ); -} diff --git a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts index 2026a74a61..0225b5da1f 100644 --- a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts +++ b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts @@ -20,7 +20,6 @@ import Debug from "debug"; import type { GraphQLFieldResolver, GraphQLResolveInfo } from "graphql"; import type { Driver } from "neo4j-driver"; -import type { Node, Relationship } from "../../../classes"; import { Executor } from "../../../classes/Executor"; import type { Neo4jDatabaseInfo } from "../../../classes/Neo4jDatabaseInfo"; import { getNeo4jDatabaseInfo } from "../../../classes/Neo4jDatabaseInfo"; @@ -37,8 +36,6 @@ const debug = Debug(DEBUG_GRAPHQL); export type WrapResolverArguments = { driver?: Driver; - nodes: Node[]; - relationships: Relationship[]; jwtPayloadFieldsMap?: Map; schemaModel: Neo4jGraphQLSchemaModel; dbInfo?: Neo4jDatabaseInfo; @@ -50,14 +47,6 @@ export type WrapResolverArguments = { * The type describing the context generated by {@link wrapQueryAndMutation}. */ export interface Neo4jGraphQLComposedContext extends Neo4jGraphQLContext { - /** - * @deprecated The use of this field is now deprecated in favour of {@link schemaModel}. - */ - nodes: Node[]; - /** - * @deprecated The use of this field is now deprecated in favour of {@link schemaModel}. - */ - relationships: Relationship[]; schemaModel: Neo4jGraphQLSchemaModel; features: ContextFeatures; executor: Executor; @@ -70,16 +59,7 @@ export interface Neo4jGraphQLComposedContext extends Neo4jGraphQLContext { let neo4jDatabaseInfo: Neo4jDatabaseInfo; export const wrapQueryAndMutation = - ({ - driver, - nodes, - relationships, - jwtPayloadFieldsMap, - schemaModel, - dbInfo, - authorization, - features, - }: WrapResolverArguments) => + ({ driver, jwtPayloadFieldsMap, schemaModel, dbInfo, authorization, features }: WrapResolverArguments) => (next: GraphQLFieldResolver) => async (root, args, context: Neo4jGraphQLContext, info: GraphQLResolveInfo) => { debugGraphQLResolveInfo(debug, info); @@ -106,7 +86,7 @@ export const wrapQueryAndMutation = cypherParams: context.cypherParams, transaction: { ...context.transaction, - metadata: {...context.transaction?.metadata, ...context.transactionMetadata}, + metadata: { ...context.transaction?.metadata, ...context.transactionMetadata }, }, }); @@ -118,8 +98,6 @@ export const wrapQueryAndMutation = } const internalContext = { - nodes, - relationships, schemaModel, features, executor, diff --git a/packages/graphql/src/schema/resolvers/mutation/create.test.ts b/packages/graphql/src/schema/resolvers/mutation/create.test.ts index 68cebb94f2..4b765f1a64 100644 --- a/packages/graphql/src/schema/resolvers/mutation/create.test.ts +++ b/packages/graphql/src/schema/resolvers/mutation/create.test.ts @@ -17,16 +17,12 @@ * limitations under the License. */ -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { createResolver } from "./create"; describe("Create resolver", () => { test("should return the correct; type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -38,7 +34,7 @@ describe("Create resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = createResolver({ node, concreteEntityAdapter }); + const result = createResolver({ concreteEntityAdapter }); expect(result.type).toBe("CreateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ diff --git a/packages/graphql/src/schema/resolvers/mutation/create.ts b/packages/graphql/src/schema/resolvers/mutation/create.ts index 7e5a45b095..8507314b7a 100644 --- a/packages/graphql/src/schema/resolvers/mutation/create.ts +++ b/packages/graphql/src/schema/resolvers/mutation/create.ts @@ -18,27 +18,23 @@ */ import { Kind, type FieldNode, type GraphQLResolveInfo } from "graphql"; -import type { Node } from "../../../classes"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { translateCreate } from "../../../translate"; +import { translateCreate } from "../../../translate/translate-create"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; -export function createResolver({ - node, - concreteEntityAdapter, -}: { - node: Node; - concreteEntityAdapter: ConcreteEntityAdapter; -}) { +export function createResolver({ concreteEntityAdapter }: { concreteEntityAdapter: ConcreteEntityAdapter }) { async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { const resolveTree = getNeo4jResolveTree(info, { args }); (context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree; - const { cypher, params } = await translateCreate({ context: context as Neo4jGraphQLTranslationContext, node }); + const { cypher, params } = await translateCreate({ + context: context as Neo4jGraphQLTranslationContext, + entityAdapter: concreteEntityAdapter, + }); const executeResult = await execute({ cypher, diff --git a/packages/graphql/src/schema/resolvers/mutation/delete.ts b/packages/graphql/src/schema/resolvers/mutation/delete.ts index 394c5ff5b2..8d53227ef1 100644 --- a/packages/graphql/src/schema/resolvers/mutation/delete.ts +++ b/packages/graphql/src/schema/resolvers/mutation/delete.ts @@ -20,7 +20,7 @@ import type { GraphQLResolveInfo } from "graphql"; import type { SchemaComposer } from "graphql-compose"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { translateDelete } from "../../../translate"; +import { translateDelete } from "../../../translate/translate-delete"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; diff --git a/packages/graphql/src/schema/resolvers/mutation/update.test.ts b/packages/graphql/src/schema/resolvers/mutation/update.test.ts index d7899edc37..e3a8cccfc5 100644 --- a/packages/graphql/src/schema/resolvers/mutation/update.test.ts +++ b/packages/graphql/src/schema/resolvers/mutation/update.test.ts @@ -17,18 +17,12 @@ * limitations under the License. */ -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { updateResolver } from "./update"; describe("Update resolver", () => { test("should return the correct; type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - // @ts-ignore - relationFields: [{}, {}], - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -40,7 +34,7 @@ describe("Update resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = updateResolver({ node, concreteEntityAdapter }); + const result = updateResolver({ concreteEntityAdapter }); expect(result.type).toBe("UpdateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ @@ -49,11 +43,6 @@ describe("Update resolver", () => { }); }); test("should return fewer fields based on number of InputTCs created", () => { - const node = new NodeBuilder({ - name: "Movie", - // @ts-ignore - relationFields: [{}, {}], - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -65,7 +54,7 @@ describe("Update resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = updateResolver({ node, concreteEntityAdapter }); + const result = updateResolver({ concreteEntityAdapter }); expect(result.type).toBe("UpdateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); diff --git a/packages/graphql/src/schema/resolvers/mutation/update.ts b/packages/graphql/src/schema/resolvers/mutation/update.ts index 366b6dc0d0..0c7bd4febc 100644 --- a/packages/graphql/src/schema/resolvers/mutation/update.ts +++ b/packages/graphql/src/schema/resolvers/mutation/update.ts @@ -17,24 +17,27 @@ * limitations under the License. */ +import Debug from "debug"; import { Kind, type FieldNode, type GraphQLResolveInfo } from "graphql"; import type { ObjectTypeComposerArgumentConfigAsObjectDefinition, ObjectTypeComposerFieldConfigAsObjectDefinition, } from "graphql-compose"; -import type { Node } from "../../../classes"; +import { DEBUG_TRANSLATE } from "../../../constants"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { translateUpdate } from "../../../translate"; +import { QueryASTFactory } from "../../../translate/queryAST/factory/QueryASTFactory"; +import { CallbackBucket } from "../../../translate/queryAST/utils/callback-bucket"; +import { buildClause } from "../../../translate/utils/build-clause"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; +const debug = Debug(DEBUG_TRANSLATE); + export function updateResolver({ - node, concreteEntityAdapter, }: { - node: Node; concreteEntityAdapter: ConcreteEntityAdapter; }): ObjectTypeComposerFieldConfigAsObjectDefinition { async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { @@ -42,7 +45,10 @@ export function updateResolver({ (context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree; - const [cypher, params] = await translateUpdate({ context: context as Neo4jGraphQLTranslationContext, node }); + const { cypher, params } = await translateUpdate({ + context: context as Neo4jGraphQLTranslationContext, + entityAdapter: concreteEntityAdapter, + }); const executeResult = await execute({ cypher, params, @@ -52,8 +58,9 @@ export function updateResolver({ }); const nodeProjection = info.fieldNodes[0]?.selectionSet?.selections.find( - (selection): selection is FieldNode => - selection.kind === Kind.FIELD && selection.name.value === concreteEntityAdapter.plural + (selection): selection is FieldNode => { + return selection.kind === Kind.FIELD && selection.name.value === concreteEntityAdapter.plural; + } ); const resolveResult = { @@ -64,7 +71,7 @@ export function updateResolver({ if (nodeProjection) { const nodeKey = nodeProjection.alias ? nodeProjection.alias.value : nodeProjection.name.value; - resolveResult[nodeKey] = executeResult.records[0]?.data || []; + resolveResult[nodeKey] = executeResult.records.map((x) => x.this); } return resolveResult; @@ -82,3 +89,29 @@ export function updateResolver({ }, }; } + +async function translateUpdate({ + context, + entityAdapter, +}: { + context: Neo4jGraphQLTranslationContext; + entityAdapter: ConcreteEntityAdapter; +}): Promise<{ cypher: string; params: Record }> { + const { resolveTree } = context; + const varName = "this"; + const operationsTreeFactory = new QueryASTFactory(context.schemaModel); + const callbackBucket = new CallbackBucket(context); + + const operationsTree = operationsTreeFactory.createMutationAST({ + resolveTree, + entityAdapter, + context, + varName, + callbackBucket, + }); + debug(operationsTree.print()); + await callbackBucket.resolveCallbacks(); + + const clause = operationsTree.build(context, varName); + return buildClause(clause, { context }); +} diff --git a/packages/graphql/src/schema/resolvers/query/cypher.test.ts b/packages/graphql/src/schema/resolvers/query/cypher.test.ts index 6b2d236a58..ec3c96cb6f 100644 --- a/packages/graphql/src/schema/resolvers/query/cypher.test.ts +++ b/packages/graphql/src/schema/resolvers/query/cypher.test.ts @@ -20,19 +20,10 @@ import { Attribute } from "../../../schema-model/attribute/Attribute"; import { GraphQLBuiltInScalarType, ScalarType } from "../../../schema-model/attribute/AttributeType"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; -import type { CypherField } from "../../../types"; import { cypherResolver } from "./cypher"; describe("Cypher resolver", () => { test("should return the correct; type, args and resolve", () => { - // @ts-ignore - const field: CypherField = { - // @ts-ignore - typeMeta: { name: "Test", pretty: "[Test]" }, - arguments: [], - isEnum: false, - isScalar: true, - }; const attribute = new Attribute({ name: "test", annotations: {}, @@ -40,7 +31,7 @@ describe("Cypher resolver", () => { args: [], }); const attributeAdapter = new AttributeAdapter(attribute); - const result = cypherResolver({ field, attributeAdapter, type: "Query" }); + const result = cypherResolver({ attributeAdapter, type: "Query" }); expect(result.type).toBe("String!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({}); diff --git a/packages/graphql/src/schema/resolvers/query/cypher.ts b/packages/graphql/src/schema/resolvers/query/cypher.ts index eeccdf3fd4..39a2bf5575 100644 --- a/packages/graphql/src/schema/resolvers/query/cypher.ts +++ b/packages/graphql/src/schema/resolvers/query/cypher.ts @@ -19,8 +19,7 @@ import type { GraphQLResolveInfo } from "graphql"; import type { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; -import { translateTopLevelCypher } from "../../../translate"; -import type { CypherField } from "../../../types"; +import { translateTopLevelCypher } from "../../../translate/translate-top-level-cypher"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; @@ -29,11 +28,9 @@ import { graphqlArgsToCompose } from "../../to-compose"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; export function cypherResolver({ - field, attributeAdapter, type, }: { - field: CypherField; // TODO: make this go away attributeAdapter: AttributeAdapter; type: "Query" | "Mutation"; }) { @@ -44,7 +41,7 @@ export function cypherResolver({ const { cypher, params } = translateTopLevelCypher({ context: context as Neo4jGraphQLTranslationContext, - field, + attributeAdapter, type, }); diff --git a/packages/graphql/src/schema/resolvers/query/fulltext.ts b/packages/graphql/src/schema/resolvers/query/fulltext.ts index 28aefb8c11..353a23c0f3 100644 --- a/packages/graphql/src/schema/resolvers/query/fulltext.ts +++ b/packages/graphql/src/schema/resolvers/query/fulltext.ts @@ -20,7 +20,7 @@ import type { GraphQLFieldResolver, GraphQLResolveInfo, SelectionSetNode } from "graphql"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import { translateRead } from "../../../translate"; +import { translateRead } from "../../../translate/translate-read"; import type { FulltextContext } from "../../../types"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; diff --git a/packages/graphql/src/schema/resolvers/query/global-node.ts b/packages/graphql/src/schema/resolvers/query/global-node.ts index 47503ec55c..2d3c34a058 100644 --- a/packages/graphql/src/schema/resolvers/query/global-node.ts +++ b/packages/graphql/src/schema/resolvers/query/global-node.ts @@ -21,7 +21,7 @@ import type { GraphQLResolveInfo } from "graphql"; import type { FieldsByTypeName } from "graphql-parse-resolve-info"; import { parseResolveInfo } from "graphql-parse-resolve-info"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { translateRead } from "../../../translate"; +import { translateRead } from "../../../translate/translate-read"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; diff --git a/packages/graphql/src/schema/resolvers/query/read.ts b/packages/graphql/src/schema/resolvers/query/read.ts index b6ce86526d..7af673ab7e 100644 --- a/packages/graphql/src/schema/resolvers/query/read.ts +++ b/packages/graphql/src/schema/resolvers/query/read.ts @@ -21,8 +21,8 @@ import { GraphQLInt, GraphQLNonNull, type GraphQLResolveInfo } from "graphql"; import type { SchemaComposer } from "graphql-compose"; import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; import { UnionEntityAdapter } from "../../../schema-model/entity/model-adapters/UnionEntityAdapter"; -import { translateRead } from "../../../translate"; import { isConcreteEntity } from "../../../translate/queryAST/utils/is-concrete-entity"; +import { translateRead } from "../../../translate/translate-read"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; diff --git a/packages/graphql/src/schema/resolvers/query/root-connection.ts b/packages/graphql/src/schema/resolvers/query/root-connection.ts index 65de64748e..ff96621895 100644 --- a/packages/graphql/src/schema/resolvers/query/root-connection.ts +++ b/packages/graphql/src/schema/resolvers/query/root-connection.ts @@ -30,7 +30,8 @@ import { PageInfo } from "../../../graphql/objects/PageInfo"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import { UnionEntityAdapter } from "../../../schema-model/entity/model-adapters/UnionEntityAdapter"; -import { translateRead } from "../../../translate"; +import type { Neo4jGraphQLSchemaModel } from "../../../schema-model/Neo4jGraphQLSchemaModel"; +import { translateRead } from "../../../translate/translate-read"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import { isNeoInt } from "../../../utils/utils"; @@ -39,7 +40,6 @@ import { createConnectionWithEdgeProperties } from "../../pagination"; import { graphqlDirectivesToCompose } from "../../to-compose"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; import { emptyConnection } from "./empty-connection"; -import type { Neo4jGraphQLSchemaModel } from "../../../schema-model/Neo4jGraphQLSchemaModel"; export function rootConnectionResolver({ composer, diff --git a/packages/graphql/src/schema/resolvers/query/vector.ts b/packages/graphql/src/schema/resolvers/query/vector.ts index e5513000d7..cd886a51c0 100644 --- a/packages/graphql/src/schema/resolvers/query/vector.ts +++ b/packages/graphql/src/schema/resolvers/query/vector.ts @@ -20,7 +20,7 @@ import type { GraphQLFieldResolver, GraphQLResolveInfo, SelectionSetNode } from "graphql"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import { translateRead } from "../../../translate"; +import { translateRead } from "../../../translate/translate-read"; import type { VectorContext } from "../../../types"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; diff --git a/packages/graphql/src/schema/resolvers/subscriptions/where/utils/type-checks.ts b/packages/graphql/src/schema/resolvers/subscriptions/where/utils/type-checks.ts deleted file mode 100644 index f8abd83ccd..0000000000 --- a/packages/graphql/src/schema/resolvers/subscriptions/where/utils/type-checks.ts +++ /dev/null @@ -1,50 +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 { int } from "neo4j-driver"; -import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { PrimitiveField } from "../../../../../types"; -import type { InterfaceSpecificType, InterfaceType, StandardType } from "../../types"; - -function isIDType(fieldMeta: PrimitiveField | undefined) { - return fieldMeta?.typeMeta.name === "ID"; -} - -export function isIDAsString(fieldMeta: PrimitiveField | undefined, value: string | number) { - return isIDType(fieldMeta) && int(value).toString() !== value; -} - -export function isInterfaceType( - node: StandardType | InterfaceType, - receivedEventRelationship: RelationshipAdapter -): node is InterfaceType { - return !!(receivedEventRelationship.target instanceof InterfaceEntityAdapter); -} - -export function isStandardType( - node: StandardType | InterfaceType, - receivedEventRelationship: RelationshipAdapter -): node is StandardType { - return !(receivedEventRelationship.target instanceof InterfaceEntityAdapter); -} - -export function isInterfaceSpecificFieldType(node: unknown): node is InterfaceSpecificType { - return !!node; -} diff --git a/packages/graphql/src/schema/get-custom-resolver-meta.ts b/packages/graphql/src/schema/selection-set-to-resolve-tree.ts similarity index 80% rename from packages/graphql/src/schema/get-custom-resolver-meta.ts rename to packages/graphql/src/schema/selection-set-to-resolve-tree.ts index 10d6ed93f3..2eab6ea536 100644 --- a/packages/graphql/src/schema/get-custom-resolver-meta.ts +++ b/packages/graphql/src/schema/selection-set-to-resolve-tree.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import type { IResolvers } from "@graphql-tools/utils"; import type { FieldDefinitionNode, InterfaceTypeDefinitionNode, @@ -28,76 +27,16 @@ import type { UnionTypeDefinitionNode, FieldNode, } from "graphql"; -import { Kind, parse } from "graphql"; +import { Kind } from "graphql"; import type { FieldsByTypeName, ResolveTree } from "graphql-parse-resolve-info"; import { generateResolveTree } from "../translate/utils/resolveTree"; -type CustomResolverMeta = { - requiredFields: Record; -}; - const INVALID_DIRECTIVES_TO_REQUIRE = ["customResolver"]; export const INVALID_REQUIRED_FIELD_ERROR = `It is not possible to require fields that use the following directives: ${INVALID_DIRECTIVES_TO_REQUIRE.map( (name) => `\`@${name}\`` ).join(", ")}`; export const INVALID_SELECTION_SET_ERROR = "Invalid selection set passed to @customResolver required"; -export function getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers, - interfaceField, -}: { - field: FieldDefinitionNode; - object: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; - objects: ObjectTypeDefinitionNode[]; - interfaces: InterfaceTypeDefinitionNode[]; - unions: UnionTypeDefinitionNode[]; - customResolvers?: IResolvers | IResolvers[]; - interfaceField?: FieldDefinitionNode; -}): CustomResolverMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "customResolver") || - interfaceField?.directives?.find((x) => x.name.value === "customResolver"); - - if (!directive) { - return undefined; - } - - if (object.kind !== Kind.INTERFACE_TYPE_DEFINITION && !customResolvers?.[field.name.value]) { - console.warn(`Custom resolver for ${field.name.value} has not been provided`); - } - - const directiveRequiresArgument = directive?.arguments?.find((arg) => arg.name.value === "requires"); - - if (!directiveRequiresArgument) { - return { - requiredFields: {}, - }; - } - - if (directiveRequiresArgument?.value.kind !== Kind.STRING) { - throw new Error("@customResolver requires expects a string"); - } - - const selectionSetDocument = parse(`{ ${directiveRequiresArgument.value.value} }`); - const requiredFieldsResolveTree = selectionSetToResolveTree( - object.fields || [], - objects, - interfaces, - unions, - selectionSetDocument - ); - if (requiredFieldsResolveTree) { - return { - requiredFields: requiredFieldsResolveTree, - }; - } -} - export function selectionSetToResolveTree( objectFields: ReadonlyArray, objects: ObjectTypeDefinitionNode[], diff --git a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts index 325028ee60..961e0b199b 100644 --- a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts +++ b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts @@ -20,7 +20,6 @@ import type { DirectiveNode } from "graphql"; import { GraphQLFloat, GraphQLNonNull } from "graphql"; import type { ObjectTypeComposer, SchemaComposer } from "graphql-compose"; -import type { SubscriptionEvents } from "../../classes/Node"; import { EventType } from "../../graphql/enums/EventType"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; @@ -28,7 +27,13 @@ import type { Neo4jFeaturesSettings, NodeSubscriptionsEvent, SubscriptionsEvent import { withWhereInputType } from "../generation/where-input"; import { generateSubscribeMethod, subscriptionResolve } from "../resolvers/subscriptions/subscribe"; import { attributeAdapterToComposeFields } from "../to-compose"; - +type SubscriptionEvents = { + create: string; + update: string; + delete: string; + create_relationship: string; + delete_relationship: string; +}; export function generateSubscriptionTypes({ schemaComposer, schemaModel, diff --git a/packages/graphql/src/translate/authorization/check-authentication.ts b/packages/graphql/src/translate/authorization/check-authentication.ts index ec0cafa6b1..6909957679 100644 --- a/packages/graphql/src/translate/authorization/check-authentication.ts +++ b/packages/graphql/src/translate/authorization/check-authentication.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import type { Node } from "../../classes"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { AuthenticationAnnotation, @@ -27,32 +26,6 @@ import { applyAuthentication } from "./utils/apply-authentication"; import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; import type { Operation } from "../../schema-model/Operation"; -export function checkAuthentication({ - context, - node, - targetOperations, - field, -}: { - context: Neo4jGraphQLTranslationContext; - node: Node; - targetOperations: AuthenticationOperation[]; - field?: string; -}) { - const concreteEntities = context.schemaModel.getEntitiesByNameAndLabels(node.name, node.getAllLabels()); - - if (concreteEntities.length !== 1) { - throw new Error("Couldn't match entity"); - } - const entity = concreteEntities[0] as ConcreteEntity; - - return checkEntityAuthentication({ - context, - entity, - targetOperations, - field, - }); -} - export function checkEntityAuthentication({ context, entity, diff --git a/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts b/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts deleted file mode 100644 index e13d399cb6..0000000000 --- a/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts +++ /dev/null @@ -1,72 +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 Cypher from "@neo4j/cypher-builder"; -import type { PredicateReturn } from "../../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import { compileCypher } from "../../../utils/compile-cypher"; -import { buildClause } from "../../utils/build-clause"; - -type CompiledPredicateReturn = { - cypher: string; - params: Record; - subqueries?: string; -}; - -/** - * Compiles Cypher, parameters and subqueries in the same Cypher Builder environment. - * - * The subqueries contain variables required by the predicate, and if they are not compiled with the same - * environment, the predicate will be referring to non-existent variables and will re-assign variable from the subqueries. - */ -export function compilePredicateReturn({ - predicateReturn, - indexPrefix, - context, -}: { - predicateReturn: PredicateReturn; - indexPrefix: string | undefined; - context: Neo4jGraphQLTranslationContext; -}): CompiledPredicateReturn { - const result: CompiledPredicateReturn = { cypher: "", params: {} }; - - const { predicate, preComputedSubqueries } = predicateReturn; - - let subqueries: string | undefined; - - if (predicate) { - const predicateCypher = new Cypher.Raw((env) => { - const predicateStr = compileCypher(predicate, env); - if (preComputedSubqueries && !preComputedSubqueries.empty) { - // Assign the Cypher string to a variable outside of the scope of the compilation - subqueries = compileCypher(preComputedSubqueries, env); - } - return predicateStr; - }); - const { cypher, params } = buildClause(predicateCypher, { - context, - prefix: `authorization_${indexPrefix || ""}`, - }); - result.cypher = cypher; - result.params = params; - result.subqueries = subqueries; - } - - return result; -} diff --git a/packages/graphql/src/translate/authorization/compatibility/create-authorization-after-and-params.ts b/packages/graphql/src/translate/authorization/compatibility/create-authorization-after-and-params.ts deleted file mode 100644 index b684001eb4..0000000000 --- a/packages/graphql/src/translate/authorization/compatibility/create-authorization-after-and-params.ts +++ /dev/null @@ -1,101 +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 Cypher from "@neo4j/cypher-builder"; -import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation"; -import type { Node } from "../../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import { - createAuthorizationAfterPredicate, - createAuthorizationAfterPredicateField, -} from "../create-authorization-after-predicate"; -import type { NodeMap } from "../types/node-map"; -import { compilePredicateReturn } from "./compile-predicate-return"; - -export type AuthorizationAfterAndParams = { - cypher: string; - params: Record; - subqueries?: string; -}; - -type StringNodeMap = { - node: Node; - variable: string; - fieldName?: string; -}; - -function stringNodeMapToNodeMap(stringNodeMap: StringNodeMap[]): NodeMap[] { - return stringNodeMap.map((nodeMap) => { - return { - ...nodeMap, - variable: new Cypher.NamedNode(nodeMap.variable), - }; - }); -} -export function createAuthorizationAfterAndParams({ - context, - nodes, - operations, - indexPrefix, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: StringNodeMap[]; - operations: AuthorizationOperation[]; - indexPrefix?: string; -}): AuthorizationAfterAndParams | undefined { - const nodeMap = stringNodeMapToNodeMap(nodes); - - const predicateReturn = createAuthorizationAfterPredicate({ - context, - nodes: nodeMap, - operations, - }); - - if (predicateReturn) { - return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context }); - } - - return undefined; -} - -export function createAuthorizationAfterAndParamsField({ - context, - nodes, - operations, - indexPrefix, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: StringNodeMap[]; - operations: AuthorizationOperation[]; - indexPrefix?: string; -}): AuthorizationAfterAndParams | undefined { - const nodeMap = stringNodeMapToNodeMap(nodes); - - const predicateReturn = createAuthorizationAfterPredicateField({ - context, - nodes: nodeMap, - operations, - }); - - if (predicateReturn) { - return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}after_`, context }); - } - - return undefined; -} diff --git a/packages/graphql/src/translate/authorization/compatibility/create-authorization-before-and-params.ts b/packages/graphql/src/translate/authorization/compatibility/create-authorization-before-and-params.ts deleted file mode 100644 index c9f7b6a79e..0000000000 --- a/packages/graphql/src/translate/authorization/compatibility/create-authorization-before-and-params.ts +++ /dev/null @@ -1,100 +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 Cypher from "@neo4j/cypher-builder"; -import type { AuthorizationOperation } from "../../../schema-model/annotation/AuthorizationAnnotation"; -import type { Node } from "../../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import { - createAuthorizationBeforePredicate, - createAuthorizationBeforePredicateField, -} from "../create-authorization-before-predicate"; -import type { NodeMap } from "../types/node-map"; -import { compilePredicateReturn } from "./compile-predicate-return"; - -type AuthorizationBeforeAndParams = { - cypher: string; - params: Record; - subqueries?: string; -}; - -type StringNodeMap = { - node: Node; - variable: string; - fieldName?: string; -}; - -function stringNodeMapToNodeMap(stringNodeMap: StringNodeMap[]): NodeMap[] { - return stringNodeMap.map((nodeMap) => { - return { - ...nodeMap, - variable: new Cypher.NamedNode(nodeMap.variable), - }; - }); -} - -export function createAuthorizationBeforeAndParams({ - context, - nodes, - operations, - indexPrefix, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: StringNodeMap[]; - operations: AuthorizationOperation[]; - indexPrefix?: string; -}): AuthorizationBeforeAndParams | undefined { - const nodeMap = stringNodeMapToNodeMap(nodes); - - const predicateReturn = createAuthorizationBeforePredicate({ - context, - nodes: nodeMap, - operations, - }); - - if (predicateReturn) { - return compilePredicateReturn({ predicateReturn, indexPrefix: `${indexPrefix || "_"}before_`, context }); - } - - return undefined; -} - -export function createAuthorizationBeforeAndParamsField({ - context, - nodes, - operations, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: StringNodeMap[]; - operations: AuthorizationOperation[]; -}): AuthorizationBeforeAndParams | undefined { - const nodeMap = stringNodeMapToNodeMap(nodes); - - const predicateReturn = createAuthorizationBeforePredicateField({ - context, - nodes: nodeMap, - operations, - }); - - if (predicateReturn) { - return compilePredicateReturn({ predicateReturn, indexPrefix: "_before_", context }); - } - - return undefined; -} diff --git a/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts b/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts deleted file mode 100644 index c95a844f57..0000000000 --- a/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts +++ /dev/null @@ -1,155 +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 Cypher from "@neo4j/cypher-builder"; -import type { AuthorizationOperation } from "../../schema-model/annotation/AuthorizationAnnotation"; -import type { PredicateReturn } from "../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; -import { getEntityAdapterFromNode } from "../../utils/get-entity-adapter-from-node"; -import { asArray } from "../../utils/utils"; -import { QueryASTContext, QueryASTEnv } from "../queryAST/ast/QueryASTContext"; -import { QueryASTFactory } from "../queryAST/factory/QueryASTFactory"; -import { isConcreteEntity } from "../queryAST/utils/is-concrete-entity"; -import { wrapSubqueryInCall } from "../queryAST/utils/wrap-subquery-in-call"; -import type { NodeMap } from "./types/node-map"; - -export function createAuthorizationAfterPredicate({ - context, - nodes, - operations, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: NodeMap[]; - operations: AuthorizationOperation[]; -}): PredicateReturn | undefined { - const predicates: Cypher.Predicate[] = []; - let subqueries: Cypher.CompositeClause | undefined; - for (const nodeEntry of nodes) { - const node = nodeEntry.node; - const matchNode = nodeEntry.variable; - - const entity = getEntityAdapterFromNode(node, context); - if (!isConcreteEntity(entity)) { - throw new Error("Expected authorization rule to be applied on a concrete entity"); - } - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: matchNode, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - - const authorizationFilters = factory.authorizationFactoryDeprecated.createAuthValidateRule({ - authAnnotation: entity.annotations.authorization, - entity, - operations, - context, - when: "AFTER", - }); - const nodeRawSubqueries = authorizationFilters?.getSubqueries(queryASTContext); - const nodeSubqueries = asArray(nodeRawSubqueries).map((sq) => wrapSubqueryInCall(sq, matchNode)); - const nodePredicate = authorizationFilters?.getPredicate(queryASTContext); - - if (nodePredicate) { - predicates.push(nodePredicate); - } - const extraSelections = authorizationFilters?.getSelection(queryASTContext); - - const preComputedSubqueries = [...asArray(extraSelections), ...asArray(nodeSubqueries)]; - if (preComputedSubqueries) { - subqueries = Cypher.utils.concat(subqueries, ...preComputedSubqueries); - } - } - if (!predicates.length) { - return; - } - return { - predicate: Cypher.and(...predicates), - preComputedSubqueries: subqueries, - }; -} - -export function createAuthorizationAfterPredicateField({ - context, - nodes, - operations, - conditionForEvaluation, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: NodeMap[]; - operations: AuthorizationOperation[]; - conditionForEvaluation?: Cypher.Predicate; -}): PredicateReturn | undefined { - const predicates: Cypher.Predicate[] = []; - let subqueries: Cypher.CompositeClause | undefined; - for (const nodeEntry of nodes) { - const node = nodeEntry.node; - const matchNode = nodeEntry.variable; - const fieldName = nodeEntry.fieldName; - - const entity = getEntityAdapterFromNode(node, context); - if (!isConcreteEntity(entity)) { - throw new Error("Expected authorization rule to be applied on a concrete entity"); - } - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: matchNode, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - - if (fieldName) { - const attributeAdapter = entity.attributes.get(fieldName); - if (!attributeAdapter) { - throw new Error("Couldn't match attribute"); - } - const attributesFilters = factory.authorizationFactoryDeprecated.createAuthValidateRule({ - authAnnotation: attributeAdapter.annotations.authorization, - entity, - operations, - context, - when: "AFTER", - conditionForEvaluation, - }); - if (attributesFilters) { - const fieldPredicate = attributesFilters.getPredicate(queryASTContext); - const fieldSelection = attributesFilters.getSelection(queryASTContext); - const fieldSubqueries = attributesFilters.getSubqueries(queryASTContext); - const preComputedSubqueries = [...asArray(fieldSelection), ...asArray(fieldSubqueries)]; - if (preComputedSubqueries) { - subqueries = Cypher.utils.concat(subqueries, ...preComputedSubqueries); - } - if (fieldPredicate) { - predicates.push(fieldPredicate); - } - } - } - } - if (!predicates.length) { - return; - } - return { - predicate: Cypher.and(...predicates), - preComputedSubqueries: subqueries, - }; -} diff --git a/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts b/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts deleted file mode 100644 index af321b0db0..0000000000 --- a/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts +++ /dev/null @@ -1,157 +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 Cypher from "@neo4j/cypher-builder"; -import type { AuthorizationOperation } from "../../schema-model/annotation/AuthorizationAnnotation"; -import type { PredicateReturn } from "../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; -import { getEntityAdapterFromNode } from "../../utils/get-entity-adapter-from-node"; -import { filterTruthy } from "../../utils/utils"; -import { QueryASTContext, QueryASTEnv } from "../queryAST/ast/QueryASTContext"; -import { QueryASTFactory } from "../queryAST/factory/QueryASTFactory"; -import { isConcreteEntity } from "../queryAST/utils/is-concrete-entity"; -import { wrapSubqueryInCall } from "../queryAST/utils/wrap-subquery-in-call"; -import type { NodeMap } from "./types/node-map"; - -export function createAuthorizationBeforePredicate({ - context, - nodes, - operations, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: NodeMap[]; - operations: AuthorizationOperation[]; -}): PredicateReturn | undefined { - const predicates: Cypher.Predicate[] = []; - let subqueries: Cypher.CompositeClause | undefined; - for (const nodeEntry of nodes) { - const node = nodeEntry.node; - const matchNode = nodeEntry.variable; - - const entity = getEntityAdapterFromNode(node, context); - if (!isConcreteEntity(entity)) { - throw new Error("Expected authorization rule to be applied on a concrete entity"); - } - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: matchNode, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - - const authorizationRules = factory.authorizationFactoryDeprecated.getAuthFilters({ - entity, - operations, - context, - }); - authorizationRules.forEach((filter) => { - const nodeRawSubqueries = filter?.getSubqueries(queryASTContext); - const nodeSubqueries = filterTruthy(nodeRawSubqueries).map((sq) => wrapSubqueryInCall(sq, matchNode)); - const nodePredicate = filter?.getPredicate(queryASTContext); - - if (nodePredicate) { - predicates.push(nodePredicate); - } - const extraSelections = filter?.getSelection(queryASTContext); - - const preComputedSubqueries = [...extraSelections, ...nodeSubqueries]; - if (preComputedSubqueries) { - subqueries = Cypher.utils.concat(subqueries, ...preComputedSubqueries); - } - }); - } - if (!predicates.length) { - return; - } - return { - predicate: Cypher.and(...predicates), - preComputedSubqueries: subqueries, - }; -} - -export function createAuthorizationBeforePredicateField({ - context, - nodes, - operations, -}: { - context: Neo4jGraphQLTranslationContext; - nodes: NodeMap[]; - operations: AuthorizationOperation[]; -}): PredicateReturn | undefined { - const predicates: Cypher.Predicate[] = []; - let subqueries: Cypher.CompositeClause | undefined; - for (const nodeEntry of nodes) { - const node = nodeEntry.node; - const matchNode = nodeEntry.variable; - const fieldName = nodeEntry.fieldName; - - const entity = getEntityAdapterFromNode(node, context); - if (!isConcreteEntity(entity)) { - throw new Error("Expected authorization rule to be applied on a concrete entity"); - } - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: matchNode, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - - if (fieldName) { - const attributeAdapter = entity.attributes.get(fieldName); - if (!attributeAdapter) { - throw new Error("Couldn't match attribute"); - } - const attributesFilters = factory.authorizationFactoryDeprecated.createAuthFilterRule({ - authAnnotation: attributeAdapter.annotations.authorization, - entity, - operations, - context, - }); - const attributesValidate = factory.authorizationFactoryDeprecated.createAuthValidateRule({ - authAnnotation: attributeAdapter.annotations.authorization, - entity, - operations, - context, - when: "BEFORE", - }); - filterTruthy([attributesFilters, attributesValidate]).forEach((filter) => { - const fieldPredicate = filter.getPredicate(queryASTContext); - const fieldSelection = filter.getSelection(queryASTContext); - const fieldSubqueries = filter.getSubqueries(queryASTContext); - - const preComputedSubqueries = [...fieldSelection, ...fieldSubqueries]; - if (preComputedSubqueries) { - subqueries = Cypher.utils.concat(subqueries, ...preComputedSubqueries); - } - if (fieldPredicate) predicates.push(fieldPredicate); - }); - } - } - if (!predicates.length) { - return; - } - return { - predicate: Cypher.and(...predicates), - preComputedSubqueries: subqueries, - }; -} diff --git a/packages/graphql/src/translate/create-connect-and-params.test.ts b/packages/graphql/src/translate/create-connect-and-params.test.ts deleted file mode 100644 index d3e0de34b6..0000000000 --- a/packages/graphql/src/translate/create-connect-and-params.test.ts +++ /dev/null @@ -1,167 +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 "graphql-tag"; -import type { Node } from "../../src/classes"; -import { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import { SchemaModelBuilder } from "../../tests/utils/builders/schema-model-builder"; -import { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import { Neo4jDatabaseInfo } from "../classes/Neo4jDatabaseInfo"; -import { RelationshipQueryDirectionOption } from "../constants"; -import { defaultNestedOperations } from "../graphql/directives/relationship"; -import type { RelationField } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import createConnectAndParams from "./create-connect-and-params"; - -describe("createConnectAndParams", () => { - let node: Node; - let context: Neo4jGraphQLTranslationContext; - - beforeAll(() => { - node = new NodeBuilder({ - name: "Movie", - enumFields: [], - scalarFields: [], - primitiveFields: [], - relationFields: [ - { - direction: "OUT", - type: "`SIMILAR`", - typeUnescaped: "SIMILAR", - fieldName: "similarMovies", - queryDirection: RelationshipQueryDirectionOption.DIRECTED, - aggregate: true, - inherited: false, - typeMeta: { - name: "Movie", - array: true, - required: false, - pretty: "[Movies]", - input: { - where: { - type: "Movie", - pretty: "[Movie]", - }, - create: { - type: "Movie", - pretty: "[Movie]", - }, - update: { - type: "Movie", - pretty: "[Movie]", - }, - }, - }, - selectableOptions: { - onRead: true, - onAggregate: false, - }, - settableOptions: { - onCreate: true, - onUpdate: true, - }, - filterableOptions: { - byValue: true, - byAggregate: true, - }, - otherDirectives: [], - arguments: [], - nestedOperations: defaultNestedOperations, - }, - ], - cypherFields: [], - temporalFields: [], - interfaceFields: [], - pointFields: [], - objectFields: [], - }).instance(); - const types = gql` - type Movie @node { - title: String! - similarMovies: [Movie!]! @relationship(type: "SIMILAR", direction: OUT) - } - `; - const schemaModel = new SchemaModelBuilder(types).instance(); - context = new ContextBuilder({ - nodes: [node], - schemaModel, - neo4jDatabaseInfo: new Neo4jDatabaseInfo("4.4.0"), - }).instance(); - }); - - test("should return the correct connection", () => { - const result = createConnectAndParams({ - withVars: ["this"], - value: [ - { - where: { node: { title_EQ: "abc" } }, - connect: { similarMovies: [{ where: { node: { title_EQ: "cba" } } }] }, - }, - ], - varName: "this", - relationField: node.relationFields[0] as RelationField, - parentVar: "this", - context, - refNodes: [node], - parentNode: node, - callbackBucket: new CallbackBucketDeprecated(context), - source: "CONNECT", - }); - - expect(result[0]).toMatchInlineSnapshot(` - "WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this0_node:Movie) - WHERE this0_node.title = $this0_node_param0 - CALL(*) { - WITH collect(this0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this0_node - CREATE (this)-[:\`SIMILAR\`]->(this0_node) - } - } - WITH this, this0_node - CALL(*) { - WITH this, this0_node - OPTIONAL MATCH (this0_node_similarMovies0_node:Movie) - WHERE this0_node_similarMovies0_node.title = $this0_node_similarMovies0_node_param0 - CALL(*) { - WITH this, collect(this0_node_similarMovies0_node) as connectedNodes, collect(this0_node) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this0_node - UNWIND connectedNodes as this0_node_similarMovies0_node - CREATE (this0_node)-[:\`SIMILAR\`]->(this0_node_similarMovies0_node) - } - } - WITH this, this0_node, this0_node_similarMovies0_node - RETURN count(*) AS connect_this0_node_similarMovies_Movie0 - } - RETURN count(*) AS connect_this_Movie0 - }" - `); - - expect(result[1]).toMatchObject({ - this0_node_param0: "abc", - this0_node_similarMovies0_node_param0: "cba", - }); - }); -}); diff --git a/packages/graphql/src/translate/create-connect-and-params.ts b/packages/graphql/src/translate/create-connect-and-params.ts deleted file mode 100644 index 216afa5e27..0000000000 --- a/packages/graphql/src/translate/create-connect-and-params.ts +++ /dev/null @@ -1,413 +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 Cypher from "@neo4j/cypher-builder"; -import type { Node, Relationship } from "../classes"; -import type { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; -import { InterfaceEntity } from "../schema-model/entity/InterfaceEntity"; -import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import type { RelationField } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { caseWhere } from "../utils/case-where"; -import { compileCypherIfExists } from "../utils/compile-cypher"; -import { getEntityAdapterFromNode } from "../utils/get-entity-adapter-from-node"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import { asArray } from "../utils/utils"; -import { checkAuthentication } from "./authorization/check-authentication"; -import { createAuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params"; -import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params"; -import { createSetRelationshipProperties } from "./create-set-relationship-properties"; -import { filterMetaVariable } from "./subscriptions/filter-meta-variable"; -import { buildClause } from "./utils/build-clause"; -import { createWhereNodePredicate } from "./where/create-where-predicate"; - -interface Res { - connects: string[]; - params: any; -} - -type Filters = { - preComputedSubqueries?: string; - predicate: [string, Record]; -}; - -function createConnectAndParams({ - withVars, - value, - varName, - relationField, - parentVar, - refNodes, - context, - callbackBucket, - labelOverride, - parentNode, - isFirstLevel = true, - source, - indexPrefix, -}: { - withVars: string[]; - value: any; - varName: string; - relationField: RelationField; - parentVar: string; - context: Neo4jGraphQLTranslationContext; - callbackBucket: CallbackBucketDeprecated; - refNodes: Node[]; - labelOverride?: string; - parentNode: Node; - isFirstLevel?: boolean; - source: "CREATE" | "UPDATE" | "CONNECT"; - indexPrefix?: string; -}): [string, any] { - checkAuthentication({ context, node: parentNode, targetOperations: ["CREATE_RELATIONSHIP"] }); - - function createSubqueryContents( - relatedNode: Node, - connect: any, - index: number, - filters?: Filters - ): { subquery: string; params: Record } { - checkAuthentication({ context, node: relatedNode, targetOperations: ["CREATE_RELATIONSHIP"] }); - - let params = {}; - const nodeName = getConnectNodeName(varName, index); - const relationshipName = getConnectEdgeName(varName, index); - const inStr = relationField.direction === "IN" ? "<-" : "-"; - const outStr = relationField.direction === "OUT" ? "->" : "-"; - const relationTypeStr = getRelationshipType(relationField, context.features); - - const relTypeStr = `[${relationField.properties ? relationshipName : ""}:${relationTypeStr}]`; - - const subquery: string[] = []; - const labels = relatedNode.getLabelString(context); - const label = labelOverride ? `:${labelOverride}` : labels; - - subquery.push(`\tWITH ${filterMetaVariable(withVars).join(", ")}`); - - subquery.push(`\tOPTIONAL MATCH (${nodeName}${label})`); - - const whereStrs: string[] = []; - if (filters) { - whereStrs.push(filters.predicate[0]); - params = { ...filters.predicate[1] }; - if (filters.preComputedSubqueries) { - subquery.push(filters.preComputedSubqueries); - } - } - - const authorizationNodes = [{ node: relatedNode, variable: nodeName }]; - // If the source is a create operation, it is likely that authorization - // rules are not satisfied until connect operation is complete - if (source !== "CREATE") { - authorizationNodes.push({ node: parentNode, variable: parentVar }); - } - - const authorizationBeforeAndParams = createAuthorizationBeforeAndParams({ - context, - nodes: authorizationNodes, - operations: ["CREATE_RELATIONSHIP"], - indexPrefix, - }); - - if (authorizationBeforeAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; - - whereStrs.push(cypher); - params = { ...params, ...authWhereParams }; - - if (subqueries) { - subquery.push(subqueries); - - if (whereStrs.length) { - subquery.push("WITH *"); - } - } - } - - if (whereStrs.length) { - const predicate = `${whereStrs.join(" AND ")}`; - if (filters?.preComputedSubqueries?.length) { - const columns = [new Cypher.NamedVariable(nodeName)]; - const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); - const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" }); - subquery.push(cypher); - } else { - subquery.push(`\tWHERE ${predicate}`); - } - } - - /* - TODO - Replace with subclauses https://neo4j.com/developer/kb/conditional-cypher-execution/ - https://neo4j.slack.com/archives/C02PUHA7C/p1603458561099100 - */ - subquery.push("\tCALL(*) {"); - const withVarsInner = [ - ...withVars.filter((v) => v !== parentVar), - `collect(${nodeName}) as connectedNodes`, - `collect(${parentVar}) as parentNodes`, - ]; - - subquery.push(`\t\tWITH ${filterMetaVariable(withVarsInner).join(", ")}`); - - subquery.push("\t\tCALL(connectedNodes, parentNodes) {"); - subquery.push(`\t\t\tUNWIND parentNodes as ${parentVar}`); - subquery.push(`\t\t\tUNWIND connectedNodes as ${nodeName}`); - subquery.push(`\t\t\tCREATE (${parentVar})${inStr}${relTypeStr}${outStr}(${nodeName})`); - - if (relationField.properties) { - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - const sourceAdapter = context.schemaModel.getConcreteEntityAdapter(relationship.source); - if (!sourceAdapter) { - throw new Error(`Transpile error: Entity with name ${relationship.source} not found`); - } - const relationshipAdapter = sourceAdapter.relationships.get(relationship.relationshipFieldName); - if (!relationshipAdapter) { - throw new Error( - `Transpile error: Relationship with name ${relationship.relationshipFieldName} not found` - ); - } - const setA = createSetRelationshipProperties({ - properties: connect.edge ?? {}, - varName: relationshipName, - relationship, - relationshipAdapter: relationshipAdapter, - operation: "CREATE", - callbackBucket, - withVars, - parameterPrefix: relationshipName, - parameterNotation: "_", - }); - if (setA) { - subquery.push(`\t\t\t${setA[0]}`); - params = { ...params, ...setA[1] }; - } - } - - subquery.push("\t\t}"); - - subquery.push("\t}"); - - const innerMetaStr = ""; - - subquery.push(`WITH ${[...filterMetaVariable(withVars), nodeName].join(", ")}${innerMetaStr}`); - - if (connect.connect) { - const connects = (Array.isArray(connect.connect) ? connect.connect : [connect.connect]) as any[]; - - connects.forEach((c) => { - const reduced = Object.entries(c).reduce( - (r: Res, [k, v]: [string, any]) => { - const relField = relatedNode.relationFields.find((x) => k === x.fieldName) as RelationField; - const newRefNodes: Node[] = []; - - if (relField.union) { - Object.keys(v).forEach((modelName) => { - newRefNodes.push(context.nodes.find((x) => x.name === modelName) as Node); - }); - } else if (relField.interface) { - (relField.interface.implementations as string[]).forEach((modelName) => { - newRefNodes.push(context.nodes.find((x) => x.name === modelName) as Node); - }); - } else { - newRefNodes.push(context.nodes.find((x) => x.name === relField.typeMeta.name) as Node); - } - - newRefNodes.forEach((newRefNode) => { - const recurse = createConnectAndParams({ - withVars: [...withVars, nodeName], - value: relField.union ? v[newRefNode.name] : v, - varName: `${nodeName}_${k}${relField.union ? `_${newRefNode.name}` : ""}`, - relationField: relField, - parentVar: nodeName, - context, - callbackBucket, - refNodes: [newRefNode], - parentNode: relatedNode, - labelOverride: relField.union ? newRefNode.name : "", - isFirstLevel: false, - source: "CONNECT", - }); - r.connects.push(recurse[0]); - r.params = { ...r.params, ...recurse[1] }; - }); - - return r; - }, - { connects: [], params: {} } - ); - - subquery.push(reduced.connects.join("\n")); - params = { ...params, ...reduced.params }; - }); - } - - const authorizationAfterAndParams = createAuthorizationAfterAndParams({ - context, - nodes: [ - { node: parentNode, variable: parentVar }, - { node: relatedNode, variable: nodeName }, - ], - operations: ["CREATE_RELATIONSHIP"], - indexPrefix, - }); - - if (authorizationAfterAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; - - if (cypher) { - if (subqueries) { - subquery.push(`WITH *`); - subquery.push(`${subqueries}`); - subquery.push(`WITH *`); - } else { - subquery.push(`WITH ${[...withVars, nodeName].join(", ")}`); - } - - subquery.push(`WHERE ${cypher}`); - params = { ...params, ...authWhereParams }; - } - } - - subquery.push(`\tRETURN count(*) AS connect_${varName}_${relatedNode.name}${index}`); - - return { subquery: subquery.join("\n"), params }; - } - - function reducer(res: Res, connect: any, index: number): Res { - if (isFirstLevel) { - res.connects.push(`WITH *`); - } - - const inner: string[] = []; - - if (relationField.interface) { - const subqueries: string[] = []; - const targetInterface = context.schemaModel.compositeEntities.find( - (x) => x.name === relationField.typeMeta.name - ); - if (!targetInterface || !(targetInterface instanceof InterfaceEntity)) { - throw new Error(`Target with name ${relationField.typeMeta.name} not found`); - } - const entity = new InterfaceEntityAdapter(targetInterface); - refNodes.forEach((refNode, i) => { - const nodeName = getConnectNodeName(varName, i); - const targetEntity = entity.concreteEntities.find((x) => x.name === refNode.name); - const filters = getFilters({ connect, context, entity, nodeName, targetEntity }); - - const subquery = createSubqueryContents(refNode, connect, i, filters); - if (subquery.subquery) { - subqueries.push(subquery.subquery); - res.params = { ...res.params, ...subquery.params }; - } - }); - if (subqueries.length > 0) { - inner.push(subqueries.join("\n}\nCALL(*) {\n\t")); - } - } else { - const targetNode = refNodes[0]; - if (!targetNode) { - throw new Error("No refNodes found"); - } - const entity = getEntityAdapterFromNode(targetNode, context); - const nodeName = getConnectNodeName(varName, index); - const filters = getFilters({ connect, context, entity, nodeName }); - const subquery = createSubqueryContents(targetNode, connect, index, filters); - inner.push(subquery.subquery); - res.params = { ...res.params, ...subquery.params }; - } - - if (inner.length > 0) { - res.connects.push("CALL(*) {"); - res.connects.push(...inner); - res.connects.push("}"); - } - - return res; - } - - const { connects, params } = asArray(value).reduce(reducer, { - connects: [], - params: {}, - }); - - return [connects.join("\n"), params]; -} - -// function to have a single source of truth for the node name of a connect operation, until the refactor to use CypherBuilder. -function getConnectNodeName(varName: string, index: number): string { - return `${varName}${index}_node`; -} -// function to have a single source of truth for the edge name of a connect operation, until the refactor to use CypherBuilder. -function getConnectEdgeName(varName: string, index: number): string { - return `${varName}${index}_relationship`; -} - -function getFilters({ - connect, - entity, - targetEntity, - context, - nodeName, -}: { - connect: any; - entity: EntityAdapter; - targetEntity?: ConcreteEntityAdapter; - context: Neo4jGraphQLTranslationContext; - nodeName: string; -}): Filters | undefined { - if (!connect.where) { - return; - } - - const targetElement = new Cypher.NamedNode(nodeName); - const whereInput = connect.where.node ?? {}; - - const { predicate: wherePredicate, preComputedSubqueries } = createWhereNodePredicate({ - entity, - context, - whereInput, - targetElement, - targetEntity, - }); - let preComputedWhereFieldsResult = ""; - - const whereCypher = new Cypher.Raw((env) => { - preComputedWhereFieldsResult = compileCypherIfExists(preComputedSubqueries, env); - const cypher = wherePredicate ? env.compile(wherePredicate) : ""; - return [cypher, {}]; - }); - - const result = buildClause(whereCypher, { context, prefix: `${nodeName}_` }); - - if (result.cypher) { - return { - predicate: [result.cypher, result.params], - preComputedSubqueries: preComputedWhereFieldsResult, - }; - } -} - -export default createConnectAndParams; diff --git a/packages/graphql/src/translate/create-create-and-params.test.ts b/packages/graphql/src/translate/create-create-and-params.test.ts deleted file mode 100644 index 97056a7923..0000000000 --- a/packages/graphql/src/translate/create-create-and-params.test.ts +++ /dev/null @@ -1,129 +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 { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; -import { Attribute } from "../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType, ScalarType } from "../schema-model/attribute/AttributeType"; -import { ConcreteEntity } from "../schema-model/entity/ConcreteEntity"; -import { trimmer } from "../utils"; -import createCreateAndParams from "./create-create-and-params"; - -describe("createCreateAndParams", () => { - test("should return the correct projection with 1 selection", () => { - const input = { - title: "some title", - }; - - const node = new NodeBuilder({ - name: "Movie", - relationFields: [], - cypherFields: [], - enumFields: [], - scalarFields: [], - primitiveFields: [ - { - fieldName: "title", - typeMeta: { - name: "String", - array: false, - required: false, - pretty: "String", - input: { - where: { - type: "String", - pretty: "String", - }, - create: { - type: "String", - pretty: "String", - }, - update: { - type: "String", - pretty: "String", - }, - }, - }, - selectableOptions: { - onRead: true, - onAggregate: false, - }, - settableOptions: { - onCreate: true, - onUpdate: true, - }, - filterableOptions: { - byValue: true, - byAggregate: true, - }, - otherDirectives: [], - arguments: [], - }, - ], - temporalFields: [], - interfaceFields: [], - objectFields: [], - pointFields: [], - }).instance(); - - const context = new ContextBuilder({ - schemaModel: new Neo4jGraphQLSchemaModel({ - concreteEntities: [ - new ConcreteEntity({ - name: "Movie", - labels: ["Movie"], - attributes: [ - new Attribute({ - name: "title", - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - annotations: {}, - args: [], - }), - ], - }), - ], - compositeEntities: [], - operations: {}, - annotations: {}, - }), - }).instance(); - - const result = createCreateAndParams({ - input, - node, - callbackBucket: new CallbackBucketDeprecated(context), - context, - varName: "this0", - withVars: ["this0"], - }); - - expect(trimmer(result.create)).toEqual( - trimmer(` - CREATE (this0:Movie) - SET this0.title = $this0_title - `) - ); - - expect(result.params).toMatchObject({ - this0_title: "some title", - }); - }); -}); diff --git a/packages/graphql/src/translate/create-create-and-params.ts b/packages/graphql/src/translate/create-create-and-params.ts deleted file mode 100644 index c913223206..0000000000 --- a/packages/graphql/src/translate/create-create-and-params.ts +++ /dev/null @@ -1,347 +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 { Node, Relationship } from "../classes"; -import type { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import mapToDbProperty from "../utils/map-to-db-property"; -import { checkAuthentication } from "./authorization/check-authentication"; -import { - createAuthorizationAfterAndParams, - createAuthorizationAfterAndParamsField, -} from "./authorization/compatibility/create-authorization-after-and-params"; -import createConnectAndParams from "./create-connect-and-params"; -import { createSetRelationshipProperties } from "./create-set-relationship-properties"; -import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update"; -import { addCallbackAndSetParam } from "./utils/callback-utils"; - -interface Res { - creates: string[]; - params: any; - meta: CreateMeta; -} - -interface CreateMeta { - authorizationPredicates: string[]; - authorizationSubqueries: string[]; -} - -type CreateAndParams = { - create: string; - params: Record; - authorizationPredicates: string[]; - authorizationSubqueries: string[]; -}; - -function createCreateAndParams({ - input, - varName, - node, - context, - callbackBucket, - withVars, - topLevelNodeVariable, - authorizationPrefix = [0], -}: { - input: any; - varName: string; - node: Node; - context: Neo4jGraphQLTranslationContext; - callbackBucket: CallbackBucketDeprecated; - withVars: string[]; - topLevelNodeVariable?: string; - //used to build authorization variable in auth subqueries - authorizationPrefix?: number[]; -}): CreateAndParams { - assertNonAmbiguousUpdate(node, input); - checkAuthentication({ context, node, targetOperations: ["CREATE"] }); - - function reducer(res: Res, [key, value]: [string, any], reducerIndex): Res { - const varNameKey = `${varName}_${key}`; - const relationField = node.relationFields.find((x) => key === x.fieldName); - const primitiveField = node.primitiveFields.find((x) => key === x.fieldName); - const pointField = node.pointFields.find((x) => key === x.fieldName); - const temporalField = node.temporalFields.find((x) => key === x.fieldName); - const dbFieldName = mapToDbProperty(node, key); - - if (primitiveField) { - checkAuthentication({ context, node, targetOperations: ["CREATE"], field: primitiveField.fieldName }); - } - - if (relationField) { - const refNodes: Node[] = []; - - if (relationField.union) { - Object.keys(value).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface?.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - refNodes.forEach((refNode, refNodeIndex) => { - const v = relationField.union ? value[refNode.name] : value; - const unionTypeName = relationField.union || relationField.interface ? refNode.name : ""; - - if (v.create) { - const isInterfaceAnArray = relationField.interface?.typeMeta.array; - const createNodeInputIsOfTypeRefNode = !!v.create.node?.[refNode.name]; - const createNodeInputKeys = createNodeInputIsOfTypeRefNode - ? Object.keys((v.create.node as any[]) || []) - : []; - const isCreatingMultipleNodesForOneToOneRel = !isInterfaceAnArray && createNodeInputKeys.length > 1; - if (isCreatingMultipleNodesForOneToOneRel) { - throw new Error( - `Relationship field "${relationField.connectionPrefix}.${ - relationField.interface?.dbPropertyName || relationField.interface?.fieldName - }" cannot have more than one node linked` - ); - } - - const creates = relationField.typeMeta.array ? v.create : [v.create]; - creates.forEach((create, createIndex) => { - if (relationField.interface && !create.node[refNode.name]) { - return; - } - - res.creates.push(`\nWITH *`); - - const baseName = `${varNameKey}${relationField.union ? "_" : ""}${unionTypeName}${createIndex}`; - const nodeName = `${baseName}_node`; - const propertiesName = `${baseName}_relationship`; - - const { - create: nestedCreate, - params, - authorizationPredicates, - authorizationSubqueries, - } = createCreateAndParams({ - input: relationField.interface ? create.node[refNode.name] : create.node, - context, - callbackBucket, - node: refNode, - varName: nodeName, - withVars: [...withVars, nodeName], - topLevelNodeVariable, - authorizationPrefix: [...authorizationPrefix, reducerIndex, createIndex, refNodeIndex], - }); - res.creates.push(nestedCreate); - res.params = { ...res.params, ...params }; - - const inStr = relationField.direction === "IN" ? "<-" : "-"; - const outStr = relationField.direction === "OUT" ? "->" : "-"; - - const fieldType = getRelationshipType(relationField, context.features); - const relationVarName = relationField.properties ? propertiesName : ""; - const relTypeStr = `[${relationVarName}:${fieldType}]`; - res.creates.push(`MERGE (${varName})${inStr}${relTypeStr}${outStr}(${nodeName})`); - - if (relationField.properties) { - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - const setA = createSetRelationshipProperties({ - properties: create.edge ?? {}, - varName: propertiesName, - withVars, - relationship, - operation: "CREATE", - callbackBucket, - parameterPrefix: propertiesName, - parameterNotation: "_", - }); - if (setA) { - res.creates.push(setA[0]); - res.params = { ...res.params, ...setA[1] }; - } - } - - if (authorizationPredicates.length) { - if (authorizationSubqueries.length) { - res.meta.authorizationSubqueries.push(...authorizationSubqueries); - } - res.meta.authorizationPredicates.push(...authorizationPredicates); - } - }); - } - - if (!relationField.interface && v.connect) { - const connectAndParams = createConnectAndParams({ - withVars, - value: v.connect, - varName: `${varNameKey}${relationField.union ? "_" : ""}${unionTypeName}_connect`, - parentVar: varName, - relationField, - context, - callbackBucket, - refNodes: [refNode], - labelOverride: unionTypeName, - parentNode: node, - source: "CREATE", - indexPrefix: makeAuthorizationParamsPrefix(authorizationPrefix), - }); - res.creates.push(connectAndParams[0]); - res.params = { ...res.params, ...connectAndParams[1] }; - } - }); - - if (relationField.interface && value.connect) { - const connectAndParams = createConnectAndParams({ - withVars, - value: value.connect, - varName: `${varNameKey}${relationField.union ? "_" : ""}_connect`, - parentVar: varName, - relationField, - context, - callbackBucket, - refNodes, - labelOverride: "", - parentNode: node, - source: "CREATE", - }); - res.creates.push(connectAndParams[0]); - res.params = { ...res.params, ...connectAndParams[1] }; - } - - return res; - } - - const authorizationAndParams = createAuthorizationAfterAndParamsField({ - context, - nodes: [ - { - variable: varName, - node, - fieldName: primitiveField?.fieldName, - }, - ], - operations: ["CREATE"], - indexPrefix: makeAuthorizationParamsPrefix(authorizationPrefix), - }); - - if (authorizationAndParams) { - const { cypher, params: authParams, subqueries } = authorizationAndParams; - - if (subqueries) { - res.meta.authorizationSubqueries.push(subqueries); - } - res.meta.authorizationPredicates.push(cypher); - res.params = { ...res.params, ...authParams }; - } - - if (pointField) { - if (pointField.typeMeta.array) { - res.creates.push(`SET ${varName}.${dbFieldName} = [p in $${varNameKey} | point(p)]`); - } else { - res.creates.push(`SET ${varName}.${dbFieldName} = point($${varNameKey})`); - } - - res.params[varNameKey] = value; - - return res; - } - - if (temporalField && ["DateTime", "Time"].includes(temporalField.typeMeta.name)) { - if (temporalField.typeMeta.array) { - res.creates.push( - `SET ${varName}.${dbFieldName} = [t in $${varNameKey} | ${temporalField.typeMeta.name.toLowerCase()}(t)]` - ); - } else { - res.creates.push( - `SET ${varName}.${dbFieldName} = ${temporalField.typeMeta.name.toLowerCase()}($${varNameKey})` - ); - } - - res.params[varNameKey] = value; - - return res; - } - - res.creates.push(`SET ${varName}.${dbFieldName} = $${varNameKey}`); - res.params[varNameKey] = value; - - return res; - } - - const labels = node.getLabelString(context); - const initial = [`CREATE (${varName}${labels})`]; - - const timestampedFields = node.temporalFields.filter( - (x) => ["DateTime", "Time"].includes(x.typeMeta.name) && x.timestamps?.includes("CREATE") - ); - timestampedFields.forEach((field) => { - // DateTime -> datetime(); Time -> time() - initial.push(`SET ${varName}.${field.dbPropertyName} = ${field.typeMeta.name.toLowerCase()}()`); - }); - - [...node.primitiveFields, ...node.temporalFields].forEach((field) => - addCallbackAndSetParam(field, varName, input, callbackBucket, initial, "CREATE") - ); - - const autogeneratedIdFields = node.primitiveFields.filter((x) => x.autogenerate); - autogeneratedIdFields.forEach((f) => { - initial.push(`SET ${varName}.${f.dbPropertyName} = randomUUID()`); - }); - - // eslint-disable-next-line prefer-const - let { creates, params, meta } = Object.entries(input).reduce(reducer, { - creates: initial, - params: {}, - meta: { - authorizationPredicates: [], - authorizationSubqueries: [], - }, - }); - - const { authorizationPredicates, authorizationSubqueries } = meta; - const authorizationAndParams = createAuthorizationAfterAndParams({ - context, - nodes: [ - { - variable: varName, - node, - }, - ], - operations: ["CREATE"], - indexPrefix: makeAuthorizationParamsPrefix(authorizationPrefix), - }); - - if (authorizationAndParams) { - const { cypher, params: authParams, subqueries } = authorizationAndParams; - if (subqueries) { - authorizationSubqueries.push(subqueries); - } - authorizationPredicates.push(cypher); - params = { ...params, ...authParams }; - } - - return { create: creates.join("\n"), params, authorizationPredicates, authorizationSubqueries }; -} - -function makeAuthorizationParamsPrefix(authorizationPrefix: number[]): string { - return `${authorizationPrefix.join("_")}_`; -} - -export default createCreateAndParams; diff --git a/packages/graphql/src/translate/create-delete-and-params.ts b/packages/graphql/src/translate/create-delete-and-params.ts deleted file mode 100644 index 04068a247b..0000000000 --- a/packages/graphql/src/translate/create-delete-and-params.ts +++ /dev/null @@ -1,237 +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 Cypher from "@neo4j/cypher-builder"; -import type { Node, Relationship } from "../classes"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { caseWhere } from "../utils/case-where"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import { checkAuthentication } from "./authorization/check-authentication"; -import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params"; -import { buildClause } from "./utils/build-clause"; -import { getRelationshipDirection } from "./utils/get-relationship-direction"; -import createConnectionWhereAndParams from "./where/create-connection-where-and-params"; - -interface Res { - strs: string[]; - params: any; -} - -function createDeleteAndParams({ - deleteInput, - varName, - node, - parentVar, - chainStr, - withVars, - context, - parameterPrefix, - recursing, -}: { - parentVar: string; - deleteInput: any; - varName: string; - chainStr?: string; - node: Node; - withVars: string[]; - context: Neo4jGraphQLTranslationContext; - parameterPrefix: string; - recursing?: boolean; -}): [string, any] { - checkAuthentication({ context, node, targetOperations: ["DELETE"] }); - - function reducer(res: Res, [key, value]: [string, any]) { - checkAuthentication({ context, node, targetOperations: ["DELETE"], field: key }); - - const relationField = node.relationFields.find((x) => key === x.fieldName); - - if (relationField) { - const refNodes: Node[] = []; - - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - if (relationField.union) { - Object.keys(value).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - const { inStr, outStr } = getRelationshipDirection(relationField); - - refNodes.forEach((refNode) => { - checkAuthentication({ context, node: refNode, targetOperations: ["DELETE"] }); - - const v = relationField.union ? value[refNode.name] : value; - const deletes = relationField.typeMeta.array ? v : [v]; - - deletes.forEach((d, index) => { - const innerStrs: string[] = []; - - const variableName = chainStr - ? `${varName}${index}` - : `${varName}_${key}${ - relationField.union || relationField.interface ? `_${refNode.name}` : "" - }${index}`; - const relationshipVariable = `${variableName}_relationship`; - const fieldType = getRelationshipType(relationField, context.features); - const relTypeStr = `[${relationshipVariable}:${fieldType}]`; - const nodeToDelete = `${variableName}_to_delete`; - const labels = refNode.getLabelString(context); - - innerStrs.push("WITH *"); - innerStrs.push("CALL(*) {"); - innerStrs.push( - `OPTIONAL MATCH (${parentVar})${inStr}${relTypeStr}${outStr}(${variableName}${labels})` - ); - - const whereStrs: string[] = []; - let aggregationWhere = false; - if (d.where) { - try { - const { - cypher: whereCypher, - subquery: preComputedSubqueries, - params: whereParams, - } = createConnectionWhereAndParams({ - nodeVariable: variableName, - whereInput: d.where, - node: refNode, - context, - relationshipVariable, - relationship, - parameterPrefix: `${parameterPrefix}${!recursing ? `.${key}` : ""}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ""}.where`, - }); - if (whereCypher) { - whereStrs.push(whereCypher); - res.params = { ...res.params, ...whereParams }; - if (preComputedSubqueries) { - innerStrs.push(preComputedSubqueries); - aggregationWhere = true; - } - } - } catch (_err) { - innerStrs.push(" \n}"); - return; - } - } - - const authorizationAndParams = createAuthorizationBeforeAndParams({ - context, - nodes: [ - { - variable: variableName, - node: refNode, - }, - ], - operations: ["DELETE"], - indexPrefix: "delete", - }); - - if (authorizationAndParams) { - const { cypher, params, subqueries } = authorizationAndParams; - - whereStrs.push(cypher); - res.params = { ...res.params, ...params }; - - if (subqueries) { - innerStrs.push(subqueries); - innerStrs.push("WITH *"); - } - } - - if (whereStrs.length) { - const predicate = `${whereStrs.join(" AND ")}`; - if (aggregationWhere) { - const columns = [ - new Cypher.NamedVariable(relationshipVariable), - new Cypher.NamedVariable(variableName), - ]; - const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); - const { cypher } = buildClause(caseWhereClause, { - context, - prefix: "aggregateWhereFilter", - }); - innerStrs.push(cypher); - } else { - innerStrs.push(`WHERE ${predicate}`); - } - } - - if (d.delete) { - const nestedDeleteInput = Object.entries(d.delete).reduce( - (d1, [k1, v1]) => ({ ...d1, [k1]: v1 }), - {} - ); - const importWithVars = [...withVars, variableName]; - - const deleteAndParams = createDeleteAndParams({ - context, - node: refNode, - deleteInput: nestedDeleteInput, - varName: variableName, - withVars: importWithVars, - parentVar: variableName, - parameterPrefix: `${parameterPrefix}${!recursing ? `.${key}` : ""}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ""}.delete`, - recursing: false, - }); - innerStrs.push(deleteAndParams[0]); - res.params = { ...res.params, ...deleteAndParams[1] }; - } - - const statements = [ - `WITH ${relationshipVariable}, collect(DISTINCT ${variableName}) AS ${nodeToDelete}`, - `CALL(${nodeToDelete}) {`, - `\tUNWIND ${nodeToDelete} AS x`, - `\tDETACH DELETE x`, - `}`, - `}`, - ]; - innerStrs.push(...statements); - - res.strs.push(...innerStrs); - }); - }); - - return res; - } - - return res; - } - - const { strs, params } = Object.entries(deleteInput).reduce(reducer, { - strs: [], - params: {}, - }); - - return [strs.join("\n"), params]; -} - -export default createDeleteAndParams; diff --git a/packages/graphql/src/translate/create-disconnect-and-params.test.ts b/packages/graphql/src/translate/create-disconnect-and-params.test.ts deleted file mode 100644 index e30aaa740e..0000000000 --- a/packages/graphql/src/translate/create-disconnect-and-params.test.ts +++ /dev/null @@ -1,165 +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 { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import { Neo4jDatabaseInfo } from "../classes/Neo4jDatabaseInfo"; -import { RelationshipQueryDirectionOption } from "../constants"; -import { defaultNestedOperations } from "../graphql/directives/relationship"; -import { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; -import { Attribute } from "../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType, ScalarType } from "../schema-model/attribute/AttributeType"; -import { ConcreteEntity } from "../schema-model/entity/ConcreteEntity"; -import type { RelationField } from "../types"; -import createDisconnectAndParams from "./create-disconnect-and-params"; - -describe("createDisconnectAndParams", () => { - test("should return the correct disconnect", () => { - const node = new NodeBuilder({ - name: "Movie", - relationFields: [ - { - direction: "OUT", - typeUnescaped: "SIMILAR", - type: "`SIMILAR`", - fieldName: "similarMovies", - queryDirection: RelationshipQueryDirectionOption.DIRECTED, - aggregate: true, - inherited: false, - typeMeta: { - name: "Movie", - array: true, - required: false, - pretty: "[Movies]", - input: { - where: { - type: "Movie", - pretty: "[Movie]", - }, - create: { - type: "Movie", - pretty: "[Movie]", - }, - update: { - type: "Movie", - pretty: "[Movie]", - }, - }, - }, - selectableOptions: { - onRead: true, - onAggregate: false, - }, - settableOptions: { - onCreate: true, - onUpdate: true, - }, - filterableOptions: { - byValue: true, - byAggregate: true, - }, - otherDirectives: [], - arguments: [], - nestedOperations: defaultNestedOperations, - }, - ], - cypherFields: [], - enumFields: [], - scalarFields: [], - primitiveFields: [], - temporalFields: [], - interfaceFields: [], - unionFields: [], - objectFields: [], - pointFields: [], - otherDirectives: [], - interfaces: [], - }).instance(); - - const context = new ContextBuilder({ - nodes: [node], - relationships: [], - neo4jDatabaseInfo: new Neo4jDatabaseInfo("4.4.0"), - schemaModel: new Neo4jGraphQLSchemaModel({ - concreteEntities: [ - new ConcreteEntity({ - name: "Movie", - labels: ["Movie"], - attributes: [ - new Attribute({ - name: "title", - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - annotations: {}, - args: [], - }), - ], - }), - ], - compositeEntities: [], - operations: {}, - annotations: {}, - }), - }).instance(); - - const result = createDisconnectAndParams({ - withVars: ["this"], - value: [ - { - where: { node: { title_EQ: "abc" } }, - disconnect: { similarMovies: [{ where: { node: { title_EQ: "cba" } } }] }, - }, - ], - varName: "this", - relationField: node.relationFields[0] as RelationField, - parentVar: "this", - context, - refNodes: [node], - parentNode: node, - parameterPrefix: "this", // TODO - }); - - expect(result[0]).toMatchInlineSnapshot(` - "WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this0_rel:\`SIMILAR\`]->(this0:Movie) - WHERE this0.title = $this0_where_Movie_this0param0 - CALL (this0, this0_rel, this) { - WITH collect(this0) as this0_x, this0_rel, this - UNWIND this0_x as x - DELETE this0_rel - } - CALL(*) { - WITH this, this0 - OPTIONAL MATCH (this0)-[this0_similarMovies0_rel:\`SIMILAR\`]->(this0_similarMovies0:Movie) - WHERE this0_similarMovies0.title = $this0_disconnect_similarMovies0_where_Movie_this0_similarMovies0param0 - CALL (this0_similarMovies0, this0_similarMovies0_rel, this0) { - WITH collect(this0_similarMovies0) as this0_similarMovies0_x, this0_similarMovies0_rel, this0 - UNWIND this0_similarMovies0_x as x - DELETE this0_similarMovies0_rel - } - RETURN count(*) AS disconnect_this0_similarMovies_Movie - } - RETURN count(*) AS disconnect_this_Movie - }" - `); - - expect(result[1]).toMatchObject({}); - }); -}); diff --git a/packages/graphql/src/translate/create-disconnect-and-params.ts b/packages/graphql/src/translate/create-disconnect-and-params.ts deleted file mode 100644 index 9681ea145e..0000000000 --- a/packages/graphql/src/translate/create-disconnect-and-params.ts +++ /dev/null @@ -1,293 +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 Cypher from "@neo4j/cypher-builder"; -import type { Node, Relationship } from "../classes"; -import type { RelationField } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { caseWhere } from "../utils/case-where"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import { checkAuthentication } from "./authorization/check-authentication"; -import { createAuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params"; -import { createAuthorizationBeforeAndParams } from "./authorization/compatibility/create-authorization-before-and-params"; -import { buildClause } from "./utils/build-clause"; -import { getRelationshipDirection } from "./utils/get-relationship-direction"; -import createConnectionWhereAndParams from "./where/create-connection-where-and-params"; - -interface Res { - disconnects: string[]; - params: any; -} - -function createDisconnectAndParams({ - withVars, - value, - varName, - relationField, - parentVar, - refNodes, - context, - labelOverride, - parentNode, - parameterPrefix, - isFirstLevel = true, -}: { - withVars: string[]; - value: any; - varName: string; - relationField: RelationField; - parentVar: string; - context: Neo4jGraphQLTranslationContext; - refNodes: Node[]; - labelOverride?: string; - parentNode: Node; - parameterPrefix: string; - isFirstLevel?: boolean; -}): [string, any] { - checkAuthentication({ context, node: parentNode, targetOperations: ["DELETE_RELATIONSHIP"] }); - - function createSubqueryContents( - relatedNode: Node, - disconnect: any, - index: number - ): { subquery: string; params: Record } { - checkAuthentication({ context, node: relatedNode, targetOperations: ["DELETE_RELATIONSHIP"] }); - - const variableName = `${varName}${index}`; - const { inStr, outStr } = getRelationshipDirection(relationField); - const relVarName = `${variableName}_rel`; - const fieldType = getRelationshipType(relationField, context.features); - const relTypeStr = `[${relVarName}:${fieldType}]`; - const subquery: string[] = []; - let params; - const labels = relatedNode.getLabelString(context); - const label = labelOverride ? `:${labelOverride}` : labels; - - subquery.push(`WITH ${withVars.join(", ")}`); - subquery.push(`OPTIONAL MATCH (${parentVar})${inStr}${relTypeStr}${outStr}(${variableName}${label})`); - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - const whereStrs: string[] = []; - let aggregationWhere = false; - if (disconnect.where) { - try { - const { - cypher: whereCypher, - subquery: preComputedSubqueries, - params: whereParams, - } = createConnectionWhereAndParams({ - nodeVariable: variableName, - whereInput: disconnect.where, - node: relatedNode, - context, - relationshipVariable: relVarName, - relationship, - parameterPrefix: `${parameterPrefix}${relationField.typeMeta.array ? `[${index}]` : ""}.where.${ - relatedNode.name - }`, - }); - if (whereCypher) { - whereStrs.push(whereCypher); - params = { ...params, ...whereParams }; - if (preComputedSubqueries) { - subquery.push(preComputedSubqueries); - aggregationWhere = true; - } - } - } catch { - return { subquery: "", params: {} }; - } - } - - const authorizationBeforeAndParams = createAuthorizationBeforeAndParams({ - context, - nodes: [ - { node: parentNode, variable: parentVar }, - { node: relatedNode, variable: variableName }, - ], - operations: ["DELETE_RELATIONSHIP"], - }); - - if (authorizationBeforeAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; - - whereStrs.push(cypher); - params = { ...params, ...authWhereParams }; - - if (subqueries) { - subquery.push(subqueries); - if (whereStrs.length) { - subquery.push("WITH *"); - } - } - } - - if (whereStrs.length) { - const predicate = `${whereStrs.join(" AND ")}`; - if (aggregationWhere) { - const columns = [new Cypher.NamedVariable(relVarName), new Cypher.NamedVariable(variableName)]; - const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); - const { cypher } = buildClause(caseWhereClause, { context, prefix: "aggregateWhereFilter" }); - subquery.push(cypher); - } else { - subquery.push(`WHERE ${predicate}`); - } - } - - subquery.push(`CALL (${variableName}, ${relVarName}, ${parentVar}) {`); - // Trick to avoid execution on null values - subquery.push(`\tWITH collect(${variableName}) as ${variableName}_x, ${relVarName}, ${parentVar}`); - subquery.push(`\tUNWIND ${variableName}_x as x`); - - subquery.push(`\tDELETE ${relVarName}`); - - subquery.push(`}`); - - // TODO - relationship validation - Blocking, if this were to be enforced it would stop someone from 'reconnecting' - - if (disconnect.disconnect) { - const disconnects: Array = Array.isArray(disconnect.disconnect) - ? disconnect.disconnect - : [disconnect.disconnect]; - - disconnects.forEach((c) => { - const reduced = Object.entries(c).reduce( - (r: Res, [k, v]: [string, any]) => { - const relField = relatedNode.relationFields.find((x) => - k.startsWith(x.fieldName) - ) as RelationField; - const newRefNodes: Node[] = []; - - if (relField.union) { - Object.keys(v).forEach((modelName) => { - newRefNodes.push(context.nodes.find((x) => x.name === modelName) as Node); - }); - } else if (relField.interface) { - (relField.interface.implementations as string[]).forEach((modelName) => { - newRefNodes.push(context.nodes.find((x) => x.name === modelName) as Node); - }); - } else { - newRefNodes.push(context.nodes.find((x) => x.name === relField.typeMeta.name) as Node); - } - - newRefNodes.forEach((newRefNode, i) => { - const recurse = createDisconnectAndParams({ - withVars: [...withVars, variableName], - value: relField.union ? v[newRefNode.name] : v, - varName: `${variableName}_${k}${relField.union ? `_${newRefNode.name}` : ""}`, - relationField: relField, - parentVar: variableName, - context, - refNodes: [newRefNode], - parentNode: relatedNode, - parameterPrefix: `${parameterPrefix}${ - relField.typeMeta.array ? `[${i}]` : "" - }.disconnect.${k}${relField.union ? `.${newRefNode.name}` : ""}`, - labelOverride: relField.union ? newRefNode.name : "", - isFirstLevel: false, - }); - r.disconnects.push(recurse[0]); - r.params = { ...r.params, ...recurse[1] }; - }); - - return r; - }, - { disconnects: [], params: {} } - ); - - subquery.push(reduced.disconnects.join("\n")); - params = { ...params, ...reduced.params }; - }); - } - - const authorizationAfterAndParams = createAuthorizationAfterAndParams({ - context, - nodes: [ - { node: parentNode, variable: parentVar }, - { node: relatedNode, variable: variableName }, - ], - operations: ["DELETE_RELATIONSHIP"], - }); - - if (authorizationAfterAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; - - if (cypher) { - if (subqueries) { - subquery.push(`WITH *`); - subquery.push(`${subqueries}`); - subquery.push(`WITH *`); - } else { - subquery.push(`WITH ${[...withVars, variableName].join(", ")}`); - } - - subquery.push(`WHERE ${cypher}`); - params = { ...params, ...authWhereParams }; - } - } - - subquery.push(`RETURN count(*) AS disconnect_${varName}_${relatedNode.name}`); - - return { subquery: subquery.join("\n"), params }; - } - - function reducer(res: Res, disconnect: { where: any; disconnect: any }, index: number): Res { - if (isFirstLevel) { - res.disconnects.push(`WITH ${withVars.join(", ")}`); - } - - const inner: string[] = []; - if (relationField.interface) { - const subqueries: string[] = []; - refNodes.forEach((refNode) => { - const subquery = createSubqueryContents(refNode, disconnect, index); - if (subquery.subquery) { - subqueries.push(subquery.subquery); - res.params = { ...res.params, ...subquery.params }; - } - }); - if (subqueries.length > 0) { - inner.push(subqueries.join("\n}\nCALL(*) {\n\t")); - } - } else { - const subquery = createSubqueryContents(refNodes[0] as Node, disconnect, index); - inner.push(subquery.subquery); - res.params = { ...res.params, ...subquery.params }; - } - - if (inner.length > 0) { - res.disconnects.push("CALL(*) {"); - res.disconnects.push(...inner); - res.disconnects.push("}"); - } - - return res; - } - - const { disconnects, params } = ((relationField.typeMeta.array ? value : [value]) as any[]).reduce(reducer, { - disconnects: [], - params: {}, - }); - - return [disconnects.join("\n"), params]; -} - -export default createDisconnectAndParams; diff --git a/packages/graphql/src/translate/create-set-relationship-properties.ts b/packages/graphql/src/translate/create-set-relationship-properties.ts deleted file mode 100644 index 88f3207244..0000000000 --- a/packages/graphql/src/translate/create-set-relationship-properties.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 { type Relationship } from "../classes"; -import type { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import type { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; -import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update"; -import { addCallbackAndSetParam } from "./utils/callback-utils"; -import { getMutationFieldStatements } from "./utils/get-mutation-field-statements"; - -export function createSetRelationshipProperties({ - properties, - varName, - withVars, - relationship, - relationshipAdapter, - operation, - callbackBucket, - parameterPrefix, - parameterNotation, - isUpdateOperation = false, -}: { - properties: Record>; - varName: string; - withVars: string[]; - relationship: Relationship; - relationshipAdapter?: RelationshipAdapter; - operation: "CREATE" | "UPDATE"; - callbackBucket: CallbackBucketDeprecated; - parameterPrefix: string; - parameterNotation: "." | "_"; - isUpdateOperation?: boolean; -}): [string, Record] | undefined { - // setting properties on the edge of an Interface relationship - // the input can contain other properties than the one applicable for this concrete entity relationship field - if (Object.keys(properties).find((k) => relationshipAdapter?.siblings?.includes(k))) { - const applicableProperties = properties[relationship.properties as string]; - if (applicableProperties) { - return createSetRelationshipPropertiesForProperties({ - properties: applicableProperties, - varName, - withVars, - relationship, - operation, - callbackBucket, - parameterPrefix: `${parameterPrefix}${parameterNotation}${relationship.properties}`, - parameterNotation, - isUpdateOperation, - }); - } - return; - } - return createSetRelationshipPropertiesForProperties({ - properties, - varName, - withVars, - relationship, - operation, - callbackBucket, - parameterPrefix, - parameterNotation, - isUpdateOperation, - }); -} - -function createSetRelationshipPropertiesForProperties({ - properties, - varName, - withVars, - relationship, - operation, - callbackBucket, - parameterPrefix, - parameterNotation, - isUpdateOperation, -}: { - properties: Record; - varName: string; - withVars: string[]; - relationship: Relationship; - operation: "CREATE" | "UPDATE"; - callbackBucket: CallbackBucketDeprecated; - parameterPrefix: string; - parameterNotation: "." | "_"; - isUpdateOperation: boolean; -}): [string, Record] { - assertNonAmbiguousUpdate(relationship, properties); - const strs: string[] = []; - const params = {}; - - addAutogenerateProperties({ relationship, operation, varName, strs }); - [...relationship.primitiveFields, ...relationship.temporalFields].forEach((field) => - addCallbackAndSetParam(field, varName, properties, callbackBucket, strs, operation) - ); - - Object.entries(properties).forEach(([key, value], _idx) => { - const param = `${parameterPrefix}${parameterNotation}${key}`; - const mutationFieldStatements = getMutationFieldStatements({ - nodeOrRel: relationship, - param, - key, - varName, - value, - withVars, - isUpdateOperation, - }); - strs.push(mutationFieldStatements); - params[param] = value; - }); - - return [strs.join("\n"), params]; -} - -function addAutogenerateProperties({ - relationship, - operation, - varName, - strs, -}: { - relationship: Relationship; - operation: "CREATE" | "UPDATE"; - varName: string; - strs: string[]; -}) { - relationship.primitiveFields.forEach((primitiveField) => { - if (primitiveField?.autogenerate) { - if (operation === "CREATE") { - strs.push(`SET ${varName}.${primitiveField.dbPropertyName} = randomUUID()`); - } - } - }); - - relationship.temporalFields.forEach((temporalField) => { - if ( - ["DateTime", "Time"].includes(temporalField.typeMeta.name) && - temporalField?.timestamps?.includes(operation) - ) { - // DateTime -> datetime(); Time -> time() - strs.push( - `SET ${varName}.${temporalField.dbPropertyName} = ${temporalField.typeMeta.name.toLowerCase()}()` - ); - } - }); -} diff --git a/packages/graphql/src/translate/create-update-and-params.test.ts b/packages/graphql/src/translate/create-update-and-params.test.ts deleted file mode 100644 index 9f56f9b71c..0000000000 --- a/packages/graphql/src/translate/create-update-and-params.test.ts +++ /dev/null @@ -1,120 +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 { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; -import { Attribute } from "../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType, ScalarType } from "../schema-model/attribute/AttributeType"; -import { ConcreteEntity } from "../schema-model/entity/ConcreteEntity"; -import type { BaseField } from "../types"; -import { trimmer } from "../utils"; -import createUpdateAndParams from "./create-update-and-params"; - -describe("createUpdateAndParams", () => { - test("should return the correct update and params", () => { - const idField: BaseField = { - fieldName: "id", - typeMeta: { - name: "String", - array: false, - required: false, - pretty: "String", - input: { - where: { - type: "String", - pretty: "String", - }, - create: { - type: "String", - pretty: "String", - }, - update: { - type: "String", - pretty: "String", - }, - }, - }, - selectableOptions: { - onRead: true, - onAggregate: false, - }, - settableOptions: { - onCreate: true, - onUpdate: true, - }, - filterableOptions: { - byValue: true, - byAggregate: true, - }, - otherDirectives: [], - arguments: [], - }; - - const node = new NodeBuilder({ - name: "Movie", - primitiveFields: [idField], - }).instance(); - - const context = new ContextBuilder({ - schemaModel: new Neo4jGraphQLSchemaModel({ - concreteEntities: [ - new ConcreteEntity({ - name: "Movie", - labels: ["Movie"], - attributes: [ - new Attribute({ - name: "id", - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - annotations: {}, - args: [], - }), - ], - }), - ], - compositeEntities: [], - operations: {}, - annotations: {}, - }), - }).instance(); - - const result = createUpdateAndParams({ - updateInput: { id_SET: "new" }, - node, - context, - varName: "this", - parentVar: "this", - withVars: ["this"], - parameterPrefix: "this", - callbackBucket: new CallbackBucketDeprecated(context), - ignoreOperationAuthorization: false, - }); - - expect(trimmer(result[0])).toEqual( - trimmer(` - SET this.id = $this_update_id_SET - `) - ); - - expect(result[1]).toMatchObject({ - this_update_id_SET: "new", - }); - }); -}); diff --git a/packages/graphql/src/translate/create-update-and-params.ts b/packages/graphql/src/translate/create-update-and-params.ts deleted file mode 100644 index c0ed5e91c0..0000000000 --- a/packages/graphql/src/translate/create-update-and-params.ts +++ /dev/null @@ -1,677 +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 Cypher from "@neo4j/cypher-builder"; -import pluralize from "pluralize"; -import type { Node, Relationship } from "../classes"; -import type { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import type { BaseField } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { caseWhere } from "../utils/case-where"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import { wrapStringInApostrophes } from "../utils/wrap-string-in-apostrophes"; -import { checkAuthentication } from "./authorization/check-authentication"; -import type { AuthorizationAfterAndParams } from "./authorization/compatibility/create-authorization-after-and-params"; -import { - createAuthorizationAfterAndParams, - createAuthorizationAfterAndParamsField, -} from "./authorization/compatibility/create-authorization-after-and-params"; -import { - createAuthorizationBeforeAndParams, - createAuthorizationBeforeAndParamsField, -} from "./authorization/compatibility/create-authorization-before-and-params"; -import createConnectAndParams from "./create-connect-and-params"; -import createCreateAndParams from "./create-create-and-params"; -import createDeleteAndParams from "./create-delete-and-params"; -import createDisconnectAndParams from "./create-disconnect-and-params"; -import { createSetRelationshipProperties } from "./create-set-relationship-properties"; -import { assertNonAmbiguousUpdate } from "./utils/assert-non-ambiguous-update"; -import { buildClause } from "./utils/build-clause"; -import { addCallbackAndSetParam } from "./utils/callback-utils"; -import { getAuthorizationStatements } from "./utils/get-authorization-statements"; -import { getMutationFieldStatements } from "./utils/get-mutation-field-statements"; -import { getRelationshipDirection } from "./utils/get-relationship-direction"; -import { indentBlock } from "./utils/indent-block"; -import { parseMutableField } from "./utils/parse-mutable-field"; -import createConnectionWhereAndParams from "./where/create-connection-where-and-params"; - -interface Res { - strs: string[]; - params: any; - meta: UpdateMeta; -} - -interface UpdateMeta { - preArrayMethodValidationStrs: [string, string][]; - authorizationBeforeSubqueries: string[]; - authorizationBeforePredicates: string[]; - authorizationAfterSubqueries: string[]; - authorizationAfterPredicates: string[]; -} - -export default function createUpdateAndParams({ - updateInput, - varName, - node, - parentVar, - chainStr, - withVars, - context, - callbackBucket, - parameterPrefix, - ignoreOperationAuthorization, -}: { - parentVar: string; - updateInput: any; - varName: string; - chainStr?: string; - node: Node; - withVars: string[]; - context: Neo4jGraphQLTranslationContext; - callbackBucket: CallbackBucketDeprecated; - parameterPrefix: string; - ignoreOperationAuthorization: boolean; -}): [string, any] { - let hasAppliedTimeStamps = false; - - assertNonAmbiguousUpdate(node, updateInput); - - checkAuthentication({ context, node, targetOperations: ["UPDATE"] }); - - function reducer(res: Res, [key, value]: [string, any]) { - let param: string; - if (chainStr) { - param = `${chainStr}_${key}`; - } else { - param = `${parentVar}_update_${key}`; - } - - const relationField = node.relationFields.find((x) => key === x.fieldName); - - if (relationField) { - const relationFieldType = getRelationshipType(relationField, context.features); - const refNodes: Node[] = []; - - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - if (relationField.union) { - Object.keys(value).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface?.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - const { inStr, outStr } = getRelationshipDirection(relationField); - - const subqueries: string[] = []; - const intermediateWithMetaStatements: string[] = []; - refNodes.forEach((refNode) => { - const v = relationField.union ? value[refNode.name] : value; - const updates = relationField.typeMeta.array ? v : [v]; - const subquery: string[] = []; - - updates.forEach((update, index) => { - const relationshipVariable = `${varName}_${relationField.typeUnescaped.toLowerCase()}${index}_relationship`; - const relTypeStr = `[${relationshipVariable}:${relationFieldType}]`; - const variableName = `${varName}_${key}${relationField.union ? `_${refNode.name}` : ""}${index}`; - - if (update.delete) { - const innerVarName = `${variableName}_delete`; - let deleteInput = { [key]: update.delete }; - if (relationField.union) { - deleteInput = { [key]: { [refNode.name]: update.delete } }; - } - - const deleteAndParams = createDeleteAndParams({ - context, - node, - deleteInput: deleteInput, - varName: innerVarName, - chainStr: innerVarName, - parentVar, - withVars, - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.typeMeta.array ? `[${index}]` : `` - }.delete`, // its use here - recursing: true, - }); - subquery.push(deleteAndParams[0]); - res.params = { ...res.params, ...deleteAndParams[1] }; - } - - if (update.disconnect) { - const disconnectAndParams = createDisconnectAndParams({ - context, - refNodes: [refNode], - value: update.disconnect, - varName: `${variableName}_disconnect`, - withVars, - parentVar, - relationField, - labelOverride: relationField.union ? refNode.name : "", - parentNode: node, - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ""}.disconnect`, - }); - subquery.push(disconnectAndParams[0]); - res.params = { ...res.params, ...disconnectAndParams[1] }; - } - - if (update.update) { - const whereStrs: string[] = []; - const delayedSubquery: string[] = []; - let aggregationWhere = false; - - if (update.update.where || update.where) { - try { - const { - cypher: whereClause, - subquery: preComputedSubqueries, - params: whereParams, - } = createConnectionWhereAndParams({ - whereInput: update.update.where || update.where, - node: refNode, - nodeVariable: variableName, - relationship, - relationshipVariable, - context, - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ``}.where`, - }); - - if (whereClause) { - whereStrs.push(whereClause); - res.params = { ...res.params, ...whereParams }; - if (preComputedSubqueries) { - delayedSubquery.push(preComputedSubqueries); - aggregationWhere = true; - } - } - } catch { - return; - } - } - - const innerUpdate: string[] = []; - if (withVars) { - innerUpdate.push(`WITH ${withVars.join(", ")}`); - } - - const labels = refNode.getLabelString(context); - innerUpdate.push( - `MATCH (${parentVar})${inStr}${relTypeStr}${outStr}(${variableName}${labels})` - ); - innerUpdate.push(...delayedSubquery); - - const authorizationBeforeAndParams = createAuthorizationBeforeAndParams({ - context, - nodes: [{ node: refNode, variable: variableName }], - operations: ["UPDATE"], - indexPrefix: "update", - }); - - if (authorizationBeforeAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; - - whereStrs.push(cypher); - res.params = { ...res.params, ...authWhereParams }; - - if (subqueries) { - innerUpdate.push(subqueries); - if (whereStrs.length) { - innerUpdate.push("WITH *"); - } - } - } - - if (whereStrs.length) { - const predicate = `${whereStrs.join(" AND ")}`; - if (aggregationWhere) { - const columns = [ - new Cypher.NamedVariable(relationshipVariable), - new Cypher.NamedVariable(variableName), - ]; - const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); - const { cypher } = buildClause(caseWhereClause, { - context, - prefix: "aggregateWhereFilter", - }); - innerUpdate.push(cypher); - } else { - innerUpdate.push(`WHERE ${predicate}`); - } - } - - if (update.update.edge) { - const entity = context.schemaModel.getConcreteEntityAdapter(node.name); - const relationshipAdapter = entity - ? entity.findRelationship(relationField.fieldName) - : undefined; - const res = createSetRelationshipProperties({ - properties: update.update.edge, - varName: relationshipVariable, - withVars: withVars, - relationship, - relationshipAdapter, - callbackBucket, - operation: "UPDATE", - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ``}.update.edge`, - parameterNotation: ".", - isUpdateOperation: true, - }); - let setProperties; - if (res) { - setProperties = res[0]; - } - if (setProperties) { - innerUpdate.push(setProperties); - } - } - - if (update.update.node) { - const nestedWithVars = [...withVars, variableName]; - - const nestedUpdateInput = Object.entries(update.update.node).reduce( - (d1, [k1, v1]) => ({ ...d1, [k1]: v1 }), - {} - ); - - const updateAndParams = createUpdateAndParams({ - context, - callbackBucket, - node: refNode, - updateInput: nestedUpdateInput, - varName: variableName, - withVars: nestedWithVars, - parentVar: variableName, - chainStr: `${param}${relationField.union ? `_${refNode.name}` : ""}${index}`, - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.union ? `.${refNode.name}` : "" - }${relationField.typeMeta.array ? `[${index}]` : ``}.update.node`, - ignoreOperationAuthorization, - }); - res.params = { ...res.params, ...updateAndParams[1] }; - innerUpdate.push(updateAndParams[0]); - } - - innerUpdate.push(`RETURN count(*) AS update_${variableName}`); - - subquery.push( - `WITH ${withVars.join(", ")}`, - "CALL(*) {", - indentBlock(innerUpdate.join("\n")), - "}" - ); - } - - if (update.connect) { - if (relationField.interface) { - if (!relationField.typeMeta.array) { - const inStr = relationField.direction === "IN" ? "<-" : "-"; - const outStr = relationField.direction === "OUT" ? "->" : "-"; - - const validatePredicates: string[] = []; - refNodes.forEach((refNode) => { - const validateRelationshipExistence = `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${refNode.name}))`; - validatePredicates.push(validateRelationshipExistence); - }); - - if (validatePredicates.length) { - subquery.push("WITH *"); - subquery.push( - `WHERE apoc.util.validatePredicate(${validatePredicates.join( - " OR " - )},'Relationship field "%s.%s" cannot have more than one node linked',["${ - relationField.connectionPrefix - }","${relationField.fieldName}"])` - ); - } - } - } - - const connectAndParams = createConnectAndParams({ - context, - callbackBucket, - refNodes: [refNode], - value: update.connect, - varName: `${variableName}_connect`, - withVars, - parentVar, - relationField, - labelOverride: relationField.union ? refNode.name : "", - parentNode: node, - source: "UPDATE", - }); - subquery.push(connectAndParams[0]); - - res.params = { ...res.params, ...connectAndParams[1] }; - } - - if (update.create) { - if (withVars) { - subquery.push(`WITH ${withVars.join(", ")}`); - } - const creates = relationField.typeMeta.array ? update.create : [update.create]; - creates.forEach((create, i) => { - const baseName = `${variableName}_create${i}`; - const nodeName = `${baseName}_node`; - const propertiesName = `${baseName}_relationship`; - - let createNodeInput = { - input: create.node, - }; - - if (relationField.interface) { - const nodeFields = create.node[refNode.name]; - if (!nodeFields) return; // Interface specific type not defined - createNodeInput = { - input: nodeFields, - }; - } - - if (!relationField.typeMeta.array) { - subquery.push("WITH *"); - - const validatePredicateTemplate = (condition: string) => - `WHERE apoc.util.validatePredicate(${condition},'Relationship field "%s.%s" cannot have more than one node linked',["${relationField.connectionPrefix}","${relationField.fieldName}"])`; - - const singleCardinalityValidationTemplate = (nodeName) => - `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${nodeName}))`; - - if (relationField.union && relationField.union.nodes) { - const validateRelationshipExistence = relationField.union.nodes.map( - singleCardinalityValidationTemplate - ); - subquery.push( - validatePredicateTemplate(validateRelationshipExistence.join(" OR ")) - ); - } else if (relationField.interface && relationField.interface.implementations) { - const validateRelationshipExistence = relationField.interface.implementations.map( - singleCardinalityValidationTemplate - ); - subquery.push( - validatePredicateTemplate(validateRelationshipExistence.join(" OR ")) - ); - } else { - const validateRelationshipExistence = singleCardinalityValidationTemplate( - refNode.name - ); - subquery.push(validatePredicateTemplate(validateRelationshipExistence)); - } - } - - const { - create: nestedCreate, - params, - authorizationPredicates, - authorizationSubqueries, - } = createCreateAndParams({ - context, - node: refNode, - callbackBucket, - varName: nodeName, - withVars: [...withVars, nodeName], - ...createNodeInput, - }); - subquery.push(nestedCreate); - res.params = { ...res.params, ...params }; - - const entity = context.schemaModel.getConcreteEntityAdapter(node.name); - const relationshipAdapter = entity - ? entity.findRelationship(relationField.fieldName) - : undefined; - - const setA = createSetRelationshipProperties({ - properties: create.edge ?? {}, - varName: propertiesName, - withVars, - relationship, - relationshipAdapter, - callbackBucket, - operation: "CREATE", - parameterPrefix: `${parameterPrefix}.${key}${ - relationField.union ? `.${refNode.name}` : "" - }[${index}].create[${i}].edge`, - parameterNotation: ".", - }); - - const relationVarName = setA ? propertiesName : ""; - subquery.push( - `MERGE (${parentVar})${inStr}[${relationVarName}:${relationFieldType}]${outStr}(${nodeName})` - ); - - if (setA) { - subquery.push(setA[0]); - } - - subquery.push( - ...getAuthorizationStatements(authorizationPredicates, authorizationSubqueries) - ); - }); - } - - if (relationField.interface) { - const returnStatement = `RETURN count(*) AS update_${varName}_${refNode.name}`; - - subquery.push(returnStatement); - } - }); - - if (subquery.length) { - subqueries.push(subquery.join("\n")); - } - }); - if (relationField.interface) { - res.strs.push(`WITH ${withVars.join(", ")}`); - res.strs.push(`CALL (${withVars.join(", ")}) {\n\t`); - const subqueriesWithMetaPassedOn = subqueries.map( - (each, i) => each + `\n}\n${intermediateWithMetaStatements[i] || ""}` - ); - res.strs.push(subqueriesWithMetaPassedOn.join(`\nCALL (${withVars.join(", ")}){\n\t`)); - } else { - res.strs.push(subqueries.join("\n")); - } - - return res; - } - - if (!hasAppliedTimeStamps) { - const timestampedFields = node.temporalFields.filter( - (temporalField) => - ["DateTime", "Time"].includes(temporalField.typeMeta.name) && - temporalField.timestamps?.includes("UPDATE") - ); - timestampedFields.forEach((field) => { - // DateTime -> datetime(); Time -> time() - res.strs.push(`SET ${varName}.${field.dbPropertyName} = ${field.typeMeta.name.toLowerCase()}()`); - }); - - hasAppliedTimeStamps = true; - } - - [...node.primitiveFields, ...node.temporalFields].forEach((field) => - addCallbackAndSetParam(field, varName, updateInput, callbackBucket, res.strs, "UPDATE") - ); - - const { settableField, operator } = parseMutableField(node, key); - - if (settableField) { - if (settableField.typeMeta.required && value === null) { - throw new Error(`Cannot set non-nullable field ${node.name}.${settableField.fieldName} to null`); - } - checkAuthentication({ context, node, targetOperations: ["UPDATE"], field: settableField.fieldName }); - if (operator === "PUSH" || operator === "POP") { - validateNonNullProperty(res, varName, settableField); - } - const mutationFieldStatements = getMutationFieldStatements({ - nodeOrRel: node, - param, - key, - varName, - value, - withVars, - isUpdateOperation: true, - }); - res.strs.push(mutationFieldStatements); - - res.params[param] = value; - - const authorizationBeforeAndParams = createAuthorizationBeforeAndParamsField({ - context, - nodes: [{ node: node, variable: varName, fieldName: settableField.fieldName }], - operations: ["UPDATE"], - }); - - if (authorizationBeforeAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationBeforeAndParams; - - res.meta.authorizationBeforePredicates.push(cypher); - - if (subqueries) { - res.meta.authorizationBeforeSubqueries.push(subqueries); - } - - res.params = { ...res.params, ...authWhereParams }; - } - - const authorizationAfterAndParams = createAuthorizationAfterAndParamsField({ - context, - nodes: [{ node: node, variable: varName, fieldName: settableField.fieldName }], - operations: ["UPDATE"], - }); - - if (authorizationAfterAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; - - res.meta.authorizationAfterPredicates.push(cypher); - - if (subqueries) { - res.meta.authorizationAfterSubqueries.push(subqueries); - } - - res.params = { ...res.params, ...authWhereParams }; - } - } - - return res; - } - - const reducedUpdate = Object.entries(updateInput as Record).reduce(reducer, { - strs: [], - meta: { - preArrayMethodValidationStrs: [], - authorizationBeforeSubqueries: [], - authorizationBeforePredicates: [], - authorizationAfterSubqueries: [], - authorizationAfterPredicates: [], - }, - params: {}, - }); - const { strs, meta } = reducedUpdate; - let params = reducedUpdate.params; - - const authorizationBeforeStrs = meta.authorizationBeforePredicates; - const authorizationBeforeSubqueries = meta.authorizationBeforeSubqueries; - const authorizationAfterStrs = meta.authorizationAfterPredicates; - const authorizationAfterSubqueries = meta.authorizationAfterSubqueries; - - const withStr = `WITH ${withVars.join(", ")}`; - let authorizationAfterAndParams: AuthorizationAfterAndParams | undefined; - - if (ignoreOperationAuthorization) { - authorizationAfterAndParams = undefined; - } else { - authorizationAfterAndParams = createAuthorizationAfterAndParams({ - context, - nodes: [{ node, variable: varName }], - operations: ["UPDATE"], - }); - - if (authorizationAfterAndParams) { - const { cypher, params: authWhereParams, subqueries } = authorizationAfterAndParams; - - if (cypher) { - if (subqueries) { - authorizationAfterSubqueries.push(subqueries); - } - - authorizationAfterStrs.push(cypher); - params = { ...params, ...authWhereParams }; - } - } - } - - const preUpdatePredicates = authorizationBeforeStrs; - - if (meta.preArrayMethodValidationStrs.length) { - const nullChecks = meta.preArrayMethodValidationStrs.map((validationStr) => `${validationStr[0]} IS NULL`); - const propertyNames = meta.preArrayMethodValidationStrs.map((validationStr) => validationStr[1]); - - preUpdatePredicates.push( - `apoc.util.validatePredicate(${nullChecks.join(" OR ")}, "${pluralize( - "Property", - propertyNames.length - )} ${propertyNames.map(() => "%s").join(", ")} cannot be NULL", [${wrapStringInApostrophes( - propertyNames - ).join(", ")}])` - ); - } - - let preUpdatePredicatesStr = ""; - let authorizationAfterStr = ""; - - if (preUpdatePredicates.length) { - if (authorizationBeforeSubqueries.length) { - preUpdatePredicatesStr = `${withStr}\n${authorizationBeforeSubqueries.join( - "\n" - )}\nWITH *\nWHERE ${preUpdatePredicates.join(" AND ")}`; - } else { - preUpdatePredicatesStr = `${withStr}\nWHERE ${preUpdatePredicates.join(" AND ")}`; - } - } - - if (authorizationAfterStrs.length) { - if (authorizationAfterSubqueries.length) { - authorizationAfterStr = `${withStr}\n${authorizationAfterSubqueries.join( - "\n" - )}\nWITH *\nWHERE ${authorizationAfterStrs.join(" AND ")}`; - } else { - authorizationAfterStr = `${withStr}\nWHERE ${authorizationAfterStrs.join(" AND ")}`; - } - } - - const statements = strs; - - return [[preUpdatePredicatesStr, ...statements, authorizationAfterStr].join("\n"), params]; -} - -function validateNonNullProperty(res: Res, varName: string, field: BaseField) { - res.meta.preArrayMethodValidationStrs.push([`${varName}.${field.dbPropertyName}`, `${field.dbPropertyName}`]); -} diff --git a/packages/graphql/src/translate/index.ts b/packages/graphql/src/translate/index.ts deleted file mode 100644 index 299bbb81ed..0000000000 --- a/packages/graphql/src/translate/index.ts +++ /dev/null @@ -1,25 +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. - */ - -export { translateAggregate } from "./translate-aggregate"; -export { default as translateCreate } from "./translate-create"; -export { translateDelete } from "./translate-delete"; -export { translateRead } from "./translate-read"; -export { translateTopLevelCypher } from "./translate-top-level-cypher"; -export { default as translateUpdate } from "./translate-update"; diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 12d22daa49..499fc6ed28 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -27,6 +27,7 @@ import { ConnectionReadOperation } from "./operations/ConnectionReadOperation"; import { DeleteOperation } from "./operations/DeleteOperation"; import { ReadOperation } from "./operations/ReadOperation"; import { TopLevelCreateMutationOperation } from "./operations/TopLevelCreateMutationOperation"; +import { TopLevelUpdateMutationOperation } from "./operations/TopLevelUpdateMutationOperation"; import { UnwindCreateOperation } from "./operations/UnwindCreateOperation"; import type { Operation, OperationTranspileResult } from "./operations/operations"; @@ -87,7 +88,8 @@ export class QueryAST { this.operation instanceof DeleteOperation || this.operation instanceof AggregationOperation || this.operation instanceof UnwindCreateOperation || - this.operation instanceof TopLevelCreateMutationOperation + this.operation instanceof TopLevelCreateMutationOperation || + this.operation instanceof TopLevelUpdateMutationOperation ) { return createNode(varName); } @@ -105,7 +107,7 @@ function getTreeLines(treeNode: QueryASTNode, depth: number = 0): string[] { const line = "────"; if (depth === 0) { - resultLines.push(`${nodeName}`); + resultLines.push(getTopLevelNodeName(nodeName)); } else if (depth === 1) { resultLines.push(`|${line} ${nodeName}`); } else { @@ -129,3 +131,19 @@ function getTreeLines(treeNode: QueryASTNode, depth: number = 0): string[] { return resultLines; } + +function getTopLevelNodeName(nodeName: string): string { + const currentMonth = new Date().getMonth(); + const isApril = currentMonth === 3; + const isOctober = currentMonth === 9; + const isDecember = currentMonth === 11; + if (isOctober) { + return `${nodeName} \u{1F383}\u{1F383}\u{1F383}`; + } else if (isDecember) { + return `${nodeName} \u{1F384}\u{1F384}\u{1F384}`; + } else if (isApril) { + return `${nodeName} \u{1F430}\u{1F430}\u{1F430}`; + } else { + return `${nodeName}`; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts b/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts index d61786cfdd..47654fe695 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters.ts @@ -62,10 +62,30 @@ export class AuthorizationFilters extends QueryASTNode { return; } + public getValidationPredicate( + context: QueryASTContext, + when: ValidateWhen = "BEFORE" + ): Cypher.Predicate | undefined { + const validationPredicate = Cypher.or( + ...this.getValidations(when).flatMap((validationRule) => validationRule.getPredicate(context)) + ); + return validationPredicate; + } + public getSubqueries(context: QueryASTContext): Cypher.Clause[] { return [...this.validations, ...this.filters].flatMap((c) => c.getSubqueries(context)); } + public getSubqueriesBefore(context: QueryASTContext): Cypher.Clause[] { + return [...this.validations.filter((v) => v.when === "BEFORE"), ...this.filters].flatMap((c) => + c.getSubqueries(context) + ); + } + + public getSubqueriesAfter(context: QueryASTContext): Cypher.Clause[] { + return [...this.validations.filter((v) => v.when === "AFTER")].flatMap((c) => c.getSubqueries(context)); + } + public getSelection(context: QueryASTContext): Array { return [...this.validations, ...this.filters].flatMap((c) => c.getSelection(context)); } diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/IdField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/IdField.ts index 2beae683f9..95e336f8bc 100644 --- a/packages/graphql/src/translate/queryAST/ast/input-fields/IdField.ts +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/IdField.ts @@ -33,10 +33,6 @@ export class IdField extends InputField { return []; } - public print(): string { - return `${super.print()} <${this.name}>`; - } - public getSetParams(queryASTContext: QueryASTContext): Cypher.SetParam[] { const target = this.getTarget(queryASTContext); const setParam: Cypher.SetParam = [target.property(this.attribute.databaseName), Cypher.randomUUID()]; diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts index fb92ce9914..76ce0a986e 100644 --- a/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts @@ -39,6 +39,10 @@ export abstract class InputField extends QueryASTNode { return []; } + public getPredicate(_queryASTContext: QueryASTContext): Cypher.Predicate | undefined { + return undefined; + } + protected getTarget(queryASTContext: QueryASTContext): Cypher.Node | Cypher.Relationship { const target = this.attachedTo === "node" ? queryASTContext.target : queryASTContext.relationship; if (!target) { diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/ParamInputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/ParamInputField.ts index 2dfea3ad96..45cc964817 100644 --- a/packages/graphql/src/translate/queryAST/ast/input-fields/ParamInputField.ts +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/ParamInputField.ts @@ -37,6 +37,8 @@ export class ParamInputField extends InputField { protected attribute: AttributeAdapter; protected inputValue: unknown; + private param: Cypher.Variable; + constructor({ attribute, attachedTo, @@ -49,6 +51,7 @@ export class ParamInputField extends InputField { super(attribute.name, attachedTo); this.attribute = attribute; this.inputValue = inputValue; + this.param = this.inputValue instanceof Cypher.Variable ? this.inputValue : new Cypher.Param(this.inputValue); } public getChildren() { @@ -59,23 +62,36 @@ export class ParamInputField extends InputField { queryASTContext: QueryASTContext, _inputVariable?: Cypher.Variable ): Cypher.SetParam[] { - const target = this.getTarget(queryASTContext); - - let rightVariable: Cypher.Expr; - if (this.inputValue instanceof Cypher.Variable) { - rightVariable = this.inputValue; - } else { - rightVariable = new Cypher.Param(this.inputValue); + const param = this.getParam(); + // This check is needed for populatedBy callbacks + if (param instanceof Cypher.Param) { + if (param.value === undefined) { + return []; + } } - - const leftExpr = target.property(this.attribute.databaseName); - const rightExpr = this.coerceReference(rightVariable); + const leftExpr = this.getLeftExpression(queryASTContext); + const rightExpr = this.getRightExpression(queryASTContext); const setField: Cypher.SetParam = [leftExpr, rightExpr]; return [setField]; } - private coerceReference( + protected getLeftExpression(queryASTContext: QueryASTContext): Cypher.Property { + return this.getTarget(queryASTContext).property(this.attribute.databaseName); + } + + protected getParam(): Cypher.Variable { + return this.param; + } + + protected getRightExpression( + _context: QueryASTContext + ): Exclude { + const rightVariable = this.getParam(); + return this.coerceReference(rightVariable); + } + + protected coerceReference( variable: Cypher.Variable | Cypher.Property ): Exclude { if (this.attribute.typeHelper.isSpatial()) { @@ -84,7 +100,7 @@ export class ParamInputField extends InputField { } const comprehensionVar = new Cypher.Variable(); const mapPoint = Cypher.point(comprehensionVar); - return new Cypher.ListComprehension(comprehensionVar, variable).map(mapPoint); + return new Cypher.ListComprehension(comprehensionVar).in(variable).map(mapPoint); } if (this.attribute.typeHelper.isDateTime()) { @@ -93,7 +109,7 @@ export class ParamInputField extends InputField { } const comprehensionVar = new Cypher.Variable(); const mapDateTime = Cypher.datetime(comprehensionVar); - return new Cypher.ListComprehension(comprehensionVar, variable).map(mapDateTime); + return new Cypher.ListComprehension(comprehensionVar).in(variable).map(mapDateTime); } if (this.attribute.typeHelper.isTime()) { @@ -102,9 +118,8 @@ export class ParamInputField extends InputField { } const comprehensionVar = new Cypher.Variable(); const mapTime = Cypher.time(comprehensionVar); - return new Cypher.ListComprehension(comprehensionVar, variable).map(mapTime); + return new Cypher.ListComprehension(comprehensionVar).in(variable).map(mapTime); } - return variable; } } diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/operators/MathInputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/MathInputField.ts new file mode 100644 index 0000000000..624b52f17d --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/MathInputField.ts @@ -0,0 +1,102 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { type QueryASTContext } from "../../QueryASTContext"; +import { ParamInputField } from "../ParamInputField"; + +type MathOperator = "increment" | "decrement" | "add" | "subtract" | "divide" | "multiply"; + +export class MathInputField extends ParamInputField { + private operation: MathOperator; + + constructor({ + attribute, + attachedTo, + inputValue, + operation, + }: { + attribute: AttributeAdapter; + attachedTo: "node" | "relationship"; + inputValue: unknown; + operation: MathOperator; + }) { + super({ attribute, attachedTo, inputValue }); + this.operation = operation; + if (operation == "divide" && inputValue === 0) { + throw new Error("Division by zero is not supported"); + } + } + + public getChildren() { + return []; + } + + public getSubqueries(queryASTContext: QueryASTContext): Cypher.Clause[] { + const prop = this.getLeftExpression(queryASTContext); + + const bitSize = this.attribute.typeHelper.isInt() ? 32 : 64; + const rightExpr = this.getRightExpression(queryASTContext); + // Avoid overflows, for 64 bit overflows, a long overflow is raised anyway by Neo4j + + const maxBit = Cypher.minus( + Cypher.pow(new Cypher.Literal(2), new Cypher.Literal(bitSize - 1)), + new Cypher.Literal(1) + ); + + return [ + Cypher.utils.concat( + Cypher.apoc.util.validate( + Cypher.isNull(prop), + "Cannot %s %s to Nan", + new Cypher.List([new Cypher.Literal(this.operation), this.getParam()]) + ), + Cypher.apoc.util.validate( + Cypher.gt(rightExpr, maxBit), + "Overflow: Value returned from operator %s is larger than %s bit", + new Cypher.List([new Cypher.Literal(this.operation), new Cypher.Literal(bitSize)]) + ) + ), + ]; + } + + protected getRightExpression( + queryASTContext: QueryASTContext + ): Exclude { + const rightVariable = super.getRightExpression(queryASTContext); + const targetProperty = this.getLeftExpression(queryASTContext); + + switch (this.operation) { + case "add": + case "increment": + return Cypher.plus(targetProperty, rightVariable); + case "decrement": + case "subtract": + return Cypher.minus(targetProperty, rightVariable); + case "divide": + return Cypher.divide(targetProperty, rightVariable); + case "multiply": + return Cypher.multiply(targetProperty, rightVariable); + + default: + throw new Error(`Unknown operation ${this.operation}`); + } + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PopInputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PopInputField.ts new file mode 100644 index 0000000000..97246dfe41 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PopInputField.ts @@ -0,0 +1,58 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { type QueryASTContext } from "../../QueryASTContext"; +import { ParamInputField } from "../ParamInputField"; + +export class PopInputField extends ParamInputField { + constructor({ + attribute, + attachedTo, + inputValue, + }: { + attribute: AttributeAdapter; + attachedTo: "node" | "relationship"; + inputValue: unknown; + }) { + super({ attribute, attachedTo, inputValue }); + } + + public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { + const expr = this.getLeftExpression(queryASTContext); + return Cypher.apoc.util.validatePredicate( + Cypher.isNull(expr), + `Property ${this.attribute.name} cannot be NULL` + ); + } + + protected getRightExpression( + queryASTContext: QueryASTContext + ): Exclude { + const rightVariable = super.getParam(); + const rightExpr = Cypher.minus(rightVariable); + const leftExpr = this.getLeftExpression(queryASTContext); + return new Cypher.Raw((context) => { + const leftExprCompiled = context.compile(leftExpr); + const poppedValueCompiled = context.compile(rightExpr); + return `${leftExprCompiled}[0..${poppedValueCompiled}]`; + }); + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PushInputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PushInputField.ts new file mode 100644 index 0000000000..be4fdf505d --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/operators/PushInputField.ts @@ -0,0 +1,52 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { type QueryASTContext } from "../../QueryASTContext"; +import { ParamInputField } from "../ParamInputField"; + +export class PushInputField extends ParamInputField { + constructor({ + attribute, + attachedTo, + inputValue, + }: { + attribute: AttributeAdapter; + attachedTo: "node" | "relationship"; + inputValue: unknown; + }) { + super({ attribute, attachedTo, inputValue }); + } + + public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { + const expr = this.getLeftExpression(queryASTContext); + return Cypher.apoc.util.validatePredicate( + Cypher.isNull(expr), + `Property ${this.attribute.name} cannot be NULL` + ); + } + + protected getRightExpression( + queryASTContext: QueryASTContext + ): Exclude { + const pushedValue = super.getRightExpression(queryASTContext); + return Cypher.plus(this.getLeftExpression(queryASTContext), pushedValue); + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts index 611e3092f9..888310d77b 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts @@ -21,15 +21,17 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; +import { checkEntityAuthentication } from "../../../authorization/check-authentication"; import { getEntityLabels } from "../../utils/create-node-from-entity"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; import type { QueryASTContext } from "../QueryASTContext"; import type { QueryASTNode } from "../QueryASTNode"; import type { Filter } from "../filters/Filter"; import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; import type { InputField } from "../input-fields/InputField"; +import { ParamInputField } from "../input-fields/ParamInputField"; import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern"; -import type { ReadOperation } from "./ReadOperation"; import { MutationOperation, type OperationTranspileResult } from "./operations"; export class ConnectOperation extends MutationOperation { @@ -38,9 +40,7 @@ export class ConnectOperation extends MutationOperation { private selectionPattern: SelectionPattern; protected readonly authFilters: AuthorizationFilters[] = []; - - // The response fields in the mutation, currently only READ operations are supported in the MutationResponse - public projectionOperations: ReadOperation[] = []; + protected readonly sourceAuthFilters: AuthorizationFilters[] = []; public readonly inputFields: Map = new Map(); private filters: Filter[] = []; @@ -68,7 +68,6 @@ export class ConnectOperation extends MutationOperation { ...this.filters, ...this.authFilters, ...this.inputFields.values(), - ...this.projectionOperations, ]); } @@ -79,6 +78,9 @@ export class ConnectOperation extends MutationOperation { public addAuthFilters(...filter: AuthorizationFilters[]) { this.authFilters.push(...filter); } + public addSourceAuthFilters(...filter: AuthorizationFilters[]) { + this.sourceAuthFilters.push(...filter); + } /** * Get and set field methods are utilities to remove duplicate fields between separate inputs @@ -98,10 +100,6 @@ export class ConnectOperation extends MutationOperation { this.filters.push(...filters); } - public addProjectionOperations(operations: ReadOperation[]) { - this.projectionOperations.push(...operations); - } - public getAuthorizationSubqueries(_context: QueryASTContext): Cypher.Clause[] { const nestedContext = this.nestedContext; @@ -124,6 +122,29 @@ export class ConnectOperation extends MutationOperation { const { nestedContext } = this.selectionPattern.apply(context); this.nestedContext = nestedContext; + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + }); + if (isConcreteEntity(this.relationship.source)) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.relationship.source.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + }); + } + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof ParamInputField) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["CREATE_RELATIONSHIP"], + field: field.name, + }); + } + }); + const matchPattern = new Cypher.Pattern(nestedContext.target, { labels: getEntityLabels(this.target, context.neo4jGraphQLContext), }); @@ -132,22 +153,21 @@ export class ConnectOperation extends MutationOperation { const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]); + const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); let matchClause: Cypher.Clause; if (filterSubqueries.length > 0) { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = Cypher.utils.concat( new Cypher.Match(matchPattern), ...filterSubqueries, new Cypher.With("*").where(predicate) ); } else { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = new Cypher.Match(matchPattern).where(predicate); } const relVar = new Cypher.Relationship(); - const relDirection = this.relationship.getCypherDirection(); + const relDirection = this.relationship.cypherDirectionFromRelDirection(); const connectPattern = new Cypher.Pattern(context.target) .related(relVar, { direction: relDirection, type: this.relationship.type }) @@ -172,34 +192,23 @@ export class ConnectOperation extends MutationOperation { ...mutationSubqueries, connectClause, ...this.getAuthorizationClausesAfter(nestedContext), // THESE ARE "AFTER" AUTH - ...this.getProjectionClause(nestedContext) + ...this.getSourceAuthorizationClausesAfter(context) // ONLY RUN "AFTER" AUTH ON THE SOURCE NODE ); const callClause = new Cypher.Call(clauses, [context.target]); - return { projectionExpr: context.returnVariable, clauses: [callClause] }; - } - - private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { - return this.projectionOperations.map((operationField) => { - return Cypher.utils.concat(...operationField.transpile(context).clauses); - }); + return { + projectionExpr: context.returnVariable, + clauses: [callClause], + }; } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { - const { selections, subqueries, predicates, validations } = this.transpileAuthClauses(context); - const predicate = Cypher.and(...predicates); - const lastSelection = selections[selections.length - 1]; - - if (!predicates.length && !validations.length) { + const { subqueries, validations } = this.transpileAuthClauses(context); + if (!validations.length) { return []; - } else { - if (lastSelection) { - lastSelection.where(predicate); - return [...subqueries, new Cypher.With("*"), ...selections, ...validations]; - } - return [...subqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; } + return [...subqueries, ...validations]; } private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { @@ -217,6 +226,21 @@ export class ConnectOperation extends MutationOperation { return []; } + private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + private transpileAuthClauses(context: QueryASTContext): { selections: (Cypher.With | Cypher.Match)[]; subqueries: Cypher.Clause[]; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts new file mode 100644 index 0000000000..0c5734d56d --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts @@ -0,0 +1,258 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { filterTruthy } from "../../../../utils/utils"; +import { checkEntityAuthentication } from "../../../authorization/check-authentication"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; +import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import type { Filter } from "../filters/Filter"; +import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; +import type { InputField } from "../input-fields/InputField"; +import { ParamInputField } from "../input-fields/ParamInputField"; +import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern"; +import { MutationOperation, type OperationTranspileResult } from "./operations"; + +export class DisconnectOperation extends MutationOperation { + public readonly target: ConcreteEntityAdapter; + public readonly relationship: RelationshipAdapter; + + private selectionPattern: SelectionPattern; + protected readonly authFilters: AuthorizationFilters[] = []; + protected readonly sourceAuthFilters: AuthorizationFilters[] = []; + + public readonly inputFields: Map = new Map(); + private filters: Filter[] = []; + + private nestedContext: QueryASTContext | undefined; + + constructor({ + target, + relationship, + selectionPattern, + }: { + target: ConcreteEntityAdapter; + selectionPattern: SelectionPattern; + relationship: RelationshipAdapter; + }) { + super(); + this.target = target; + this.relationship = relationship; + this.selectionPattern = selectionPattern; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([ + this.selectionPattern, + ...this.filters, + ...this.authFilters, + ...this.inputFields.values(), + ]); + } + + public print(): string { + return `${super.print()} <${this.target.name}>`; + } + + public addAuthFilters(...filter: AuthorizationFilters[]) { + this.authFilters.push(...filter); + } + public addSourceAuthFilters(...filter: AuthorizationFilters[]) { + this.sourceAuthFilters.push(...filter); + } + + /** + * Get and set field methods are utilities to remove duplicate fields between separate inputs + */ + public getField(key: string, attachedTo: "node" | "relationship") { + return this.inputFields.get(`${attachedTo}_${key}`); + } + + public addField(field: InputField, attachedTo: "node" | "relationship") { + if (!this.inputFields.has(field.name)) { + this.inputFields.set(`${attachedTo}_${field.name}`, field); + } + } + + public addFilters(...filters: Filter[]): void { + this.filters.push(...filters); + } + + public getAuthorizationSubqueries(_context: QueryASTContext): Cypher.Clause[] { + const nestedContext = this.nestedContext; + + if (!nestedContext) { + throw new Error( + "Error parsing query, nested context not available, need to call transpile first. Please contact support" + ); + } + + return [...this.inputFields.values()].flatMap((inputField) => { + return inputField.getAuthorizationSubqueries(nestedContext); + }); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + + const { nestedContext, pattern: matchPattern } = this.selectionPattern.apply(context); + this.nestedContext = nestedContext; + + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + }); + if (isConcreteEntity(this.relationship.source)) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.relationship.source.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + }); + } + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof ParamInputField) { + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["DELETE_RELATIONSHIP"], + field: field.name, + }); + } + }); + + const allFilters = [...this.authFilters, ...this.filters]; + + const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]); + + const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); + let matchClause: Cypher.Clause; + if (filterSubqueries.length > 0) { + matchClause = Cypher.utils.concat( + new Cypher.OptionalMatch(matchPattern), + ...filterSubqueries, + new Cypher.With("*").where(predicate) + ); + } else { + matchClause = new Cypher.OptionalMatch(matchPattern).where(predicate); + } + + const relVar = new Cypher.Relationship(); + + const disconnectContext = context.push({ target: nestedContext.target, relationship: relVar }); + + const mutationSubqueries = Array.from(this.inputFields.values()) + .flatMap((input) => { + return input.getSubqueries(disconnectContext); + }) + .map((sq) => new Cypher.Call(sq, [disconnectContext.target])); + + const deleteClause = new Cypher.With("*").delete(nestedContext.relationship!); + + const clauses = Cypher.utils.concat( + matchClause, + ...this.getAuthorizationClauses(nestedContext), + ...this.getSourceAuthorizationClauses(context, "BEFORE"), + ...mutationSubqueries, + deleteClause, + ...this.getAuthorizationClausesAfter(nestedContext), + ...this.getSourceAuthorizationClauses(context, "AFTER") + ); + + const callClause = new Cypher.Call(clauses, [context.target]); + + return { + projectionExpr: context.returnVariable, + clauses: [callClause], + }; + } + + private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { + const { subqueries, validations } = this.transpileAuthClauses(context); + if (!validations.length) { + return []; + } + return [...subqueries, ...validations]; + } + + private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.authFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + + private getSourceAuthorizationClauses(context: QueryASTContext, when: "BEFORE" | "AFTER"): Cypher.Clause[] { + const validations: Cypher.VoidProcedure[] = []; + for (const authFilter of this.sourceAuthFilters) { + const validation = authFilter.getValidation(context, when); + if (validation) { + validations.push(validation); + } + } + + if (validations.length > 0) { + return [new Cypher.With("*"), ...validations]; + } + return []; + } + + private transpileAuthClauses(context: QueryASTContext): { + selections: (Cypher.With | Cypher.Match)[]; + subqueries: Cypher.Clause[]; + predicates: Cypher.Predicate[]; + validations: Cypher.VoidProcedure[]; + } { + const selections: (Cypher.With | Cypher.Match)[] = []; + const subqueries: Cypher.Clause[] = []; + const predicates: Cypher.Predicate[] = []; + const validations: Cypher.VoidProcedure[] = []; + for (const authFilter of this.authFilters) { + const extraSelections = authFilter.getSelection(context); + const authSubqueries = authFilter.getSubqueries(context); + const validation = authFilter.getValidation(context, "BEFORE"); + + if (extraSelections) { + selections.push(...extraSelections); + } + if (authSubqueries) { + subqueries.push(...authSubqueries); + } + + if (validation) { + validations.push(validation); + } + } + return { selections, subqueries, predicates, validations }; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/TopLevelUpdateMutationOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/TopLevelUpdateMutationOperation.ts new file mode 100644 index 0000000000..221f601416 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/TopLevelUpdateMutationOperation.ts @@ -0,0 +1,83 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import { filterTruthy } from "../../../../utils/utils"; +import type { OperationField } from "../fields/OperationField"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import { Operation, type OperationTranspileResult } from "./operations"; +import type { UpdateOperation } from "./UpdateOperation"; + +/** Wrapper over TopLevelUpdateMutationOperation for top level update, that support multiple update operations + * This extends Operation because we don't need the mutationOperation API for top level + */ +export class TopLevelUpdateMutationOperation extends Operation { + // The response fields in the mutation, currently only READ operations are supported in the MutationResponse + private readonly projectionOperations: OperationField[]; + + private readonly updateOperations: UpdateOperation[] = []; + + constructor({ + updateOperations, + projectionOperations, + }: { + updateOperations: UpdateOperation[]; + projectionOperations: OperationField[]; + }) { + super(); + this.updateOperations = updateOperations; + this.projectionOperations = projectionOperations; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...this.updateOperations, ...this.projectionOperations]); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + context.env.topLevelOperationName = "UPDATE"; + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + const subqueries = this.updateOperations.map((field) => { + const { clauses } = field.transpile(context); + + return Cypher.utils.concat(...clauses, ...field.getAuthorizationSubqueries(context)); + }); + + const projection: Cypher.Clause = this.getProjectionClause(context); + return { + projectionExpr: context.returnVariable, + clauses: [...subqueries, projection], + }; + } + + private getProjectionClause(context: QueryASTContext): Cypher.Clause { + const projectionOperation = this.projectionOperations[0]; // TODO: multiple projection operations not supported + + if (!projectionOperation) { + return new Cypher.Finish(); + } + + const result = projectionOperation.operation.transpile(context); + + const extraWith = new Cypher.With(context.target); + return Cypher.utils.concat(extraWith, ...result.clauses); + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts index 7e5cffb31b..0e3c916542 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts @@ -20,43 +20,247 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { filterTruthy } from "../../../../utils/utils"; +import { checkEntityAuthentication } from "../../../authorization/check-authentication"; import type { QueryASTContext } from "../QueryASTContext"; import type { QueryASTNode } from "../QueryASTNode"; +import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; +import type { InputField } from "../input-fields/InputField"; +import type { SelectionPattern } from "../selection/SelectionPattern/SelectionPattern"; import type { ReadOperation } from "./ReadOperation"; import { Operation, type OperationTranspileResult } from "./operations"; -/** - * This is currently just a dummy tree node, - * The whole mutation part is still implemented in the old way, the current scope of this node is just to contains the nested fields. - **/ +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; +import type { Filter } from "../filters/Filter"; +import { ParamInputField } from "../input-fields/ParamInputField"; + export class UpdateOperation extends Operation { public readonly target: ConcreteEntityAdapter; + public readonly relationship: RelationshipAdapter | undefined; + + protected readonly authFilters: AuthorizationFilters[] = []; + protected filters: Filter[] = []; + + private readonly selectionPattern: SelectionPattern; + private readonly inputFields: InputField[] = []; // The response fields in the mutation, currently only READ operations are supported in the MutationResponse public projectionOperations: ReadOperation[] = []; + private nestedContext: QueryASTContext | undefined; - constructor({ target }: { target: ConcreteEntityAdapter }) { + constructor({ + target, + relationship, + selectionPattern, + }: { + target: ConcreteEntityAdapter; + relationship?: RelationshipAdapter; + selectionPattern: SelectionPattern; + }) { super(); this.target = target; + this.relationship = relationship; + this.selectionPattern = selectionPattern; + } + /** Prints the name of the Node */ + public print(): string { + return `${super.print()} <${this.target.name}>`; } public getChildren(): QueryASTNode[] { - return filterTruthy(this.projectionOperations); + return filterTruthy([ + this.selectionPattern, + ...this.inputFields, + ...this.filters, + ...this.authFilters, + ...this.projectionOperations, + ]); } public addProjectionOperations(operations: ReadOperation[]) { this.projectionOperations.push(...operations); } + public addAuthFilters(...filter: AuthorizationFilters[]) { + this.authFilters.push(...filter); + } + + public addField(field: InputField) { + this.inputFields.push(field); + } + + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); + } public transpile(context: QueryASTContext): OperationTranspileResult { if (!context.target) throw new Error("No parent node found!"); context.env.topLevelOperationName = "UPDATE"; - const clauses = this.getProjectionClause(context); - return { projectionExpr: context.returnVariable, clauses }; + + const { nestedContext, pattern } = this.selectionPattern.apply(context); + this.nestedContext = nestedContext; + + checkEntityAuthentication({ + context: context.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["UPDATE"], + }); + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof ParamInputField) { + checkEntityAuthentication({ + context: context.neo4jGraphQLContext, + entity: this.target.entity, + targetOperations: ["UPDATE"], + field: field.name, + }); + } + }); + + const setParams = Array.from(this.inputFields.values()).flatMap((input) => { + return input.getSetParams(nestedContext); + }); + + const mutationSubqueries = Array.from(this.inputFields.values()) + .flatMap((input) => { + const subqueries = input.getSubqueries(nestedContext); + const authSubqueries = input.getAuthorizationSubqueries(nestedContext); + if (!authSubqueries.length && !subqueries.length) { + return undefined; + } + if (authSubqueries.length && subqueries.length) { + return Cypher.utils.concat(...subqueries, new Cypher.With("*"), ...authSubqueries); + } + return Cypher.utils.concat(...subqueries, ...authSubqueries); + }) + .filter((s) => s !== undefined); + + // This is a small optimization, to avoid subqueries with no changes + // Top level should still be generated for projection + if (this.relationship) { + if (setParams.length === 0 && mutationSubqueries.length === 0) { + return { projectionExpr: nestedContext.target, clauses: [] }; + } + } + + // We need to call the filter subqueries before predicate to handle aggregate filters + const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); + + const authBeforeClauses = this.getAuthorizationClauses(nestedContext); + + const afterFilterSubqueries = this.authFilters + .flatMap((af) => af.getSubqueriesAfter(nestedContext)) + .map((sq) => { + return new Cypher.Call(sq, [nestedContext.target]); + }); + + const predicate = this.getPredicate(nestedContext); + + const matchClause = new Cypher.Match(pattern); + const filtersWith = new Cypher.With("*").where(predicate); + if (authBeforeClauses.length > 0) { + filtersWith.with("*"); + } + + let withAndSet: Cypher.Clause | undefined; + if (authBeforeClauses.length === 0) { + filtersWith.set(...setParams); + } else { + withAndSet = new Cypher.With("*").set(...setParams); + } + + const clauses = Cypher.utils.concat( + matchClause, + ...filterSubqueries, + filtersWith, + ...authBeforeClauses, + withAndSet, + afterFilterSubqueries.length > 0 ? new Cypher.With("*") : undefined, + ...mutationSubqueries.map((sq) => Cypher.utils.concat(new Cypher.With("*"), new Cypher.Call(sq, "*"))), + ...afterFilterSubqueries, + ...this.getAuthorizationClausesAfter(nestedContext) // THESE ARE "AFTER" AUTH + ); + + return { projectionExpr: nestedContext.target, clauses: [clauses] }; + } + + /** Post subqueries */ + public getAuthorizationSubqueries(_context: QueryASTContext): Cypher.Clause[] { + const nestedContext = this.nestedContext; + + if (!nestedContext) { + throw new Error( + "Error parsing query, nested context not available, need to call transpile first. Please contact support" + ); + } + + return []; } - private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { - return this.projectionOperations.map((operationField) => { - return Cypher.utils.concat(...operationField.transpile(context).clauses); + private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { + const { subqueries, validations } = this.transpileAuthClauses(context); + const authSubqueries = subqueries.map((sq) => { + return new Cypher.Call(sq, "*"); }); + if (!validations.length) { + return []; + } + return [...authSubqueries, ...validations]; + } + + private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { + const validationsAfter: Cypher.VoidProcedure[] = []; + for (const authFilter of this.authFilters) { + const validationAfter = authFilter.getValidation(context, "AFTER"); + if (validationAfter) { + validationsAfter.push(validationAfter); + } + } + + if (validationsAfter.length > 0) { + return [new Cypher.With("*"), ...validationsAfter]; + } + return []; + } + + private transpileAuthClauses(context: QueryASTContext): { + selections: (Cypher.With | Cypher.Match)[]; + subqueries: Cypher.Clause[]; + predicates: Cypher.Predicate[]; + validations: Cypher.VoidProcedure[]; + } { + const selections: (Cypher.With | Cypher.Match)[] = []; + const subqueries: Cypher.Clause[] = []; + const predicates: Cypher.Predicate[] = []; + const validations: Cypher.VoidProcedure[] = []; + for (const authFilter of this.authFilters) { + const extraSelections = authFilter.getSelection(context); + const authSubqueries = authFilter.getSubqueriesBefore(context); + const authPredicate = authFilter.getPredicate(context); + const validationBefore = authFilter.getValidation(context, "BEFORE"); + if (extraSelections) { + selections.push(...extraSelections); + } + if (authSubqueries) { + subqueries.push(...authSubqueries); + } + if (authPredicate) { + predicates.push(authPredicate); + } + if (validationBefore) { + validations.push(validationBefore); + } + } + return { selections, subqueries, predicates, validations }; + } + + private getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { + const authBeforePredicates = this.getAuthFilterPredicate(queryASTContext); + return Cypher.and( + ...this.filters.map((f) => f.getPredicate(queryASTContext)), + ...this.inputFields.map((f) => f.getPredicate(queryASTContext)), + ...authBeforePredicates + ); + } + + private getAuthFilterPredicate(context: QueryASTContext): Cypher.Predicate[] { + return filterTruthy(this.authFilters.map((f) => f.getPredicate(context))); } } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectOperation.ts new file mode 100644 index 0000000000..f1889cb46c --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectOperation.ts @@ -0,0 +1,69 @@ +/* + * 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 { Clause } from "@neo4j/cypher-builder"; +import type { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { UnionEntityAdapter } from "../../../../../schema-model/entity/model-adapters/UnionEntityAdapter"; +import { filterTruthy } from "../../../../../utils/utils"; +import type { QueryASTContext } from "../../QueryASTContext"; +import type { QueryASTNode } from "../../QueryASTNode"; +import type { OperationTranspileResult } from "../operations"; +import { MutationOperation } from "../operations"; +import type { CompositeDisconnectPartial } from "./CompositeDisconnectPartial"; + +export class CompositeDisconnectOperation extends MutationOperation { + private partials: CompositeDisconnectPartial[] = []; + private target: InterfaceEntityAdapter | UnionEntityAdapter; + + constructor({ + partials, + target, + }: { + partials: CompositeDisconnectPartial[]; + target: InterfaceEntityAdapter | UnionEntityAdapter; + }) { + super(); + this.partials = partials; + this.target = target; + } + + public print(): string { + return `${super.print()} <${this.target.name}>`; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...this.partials]); + } + + transpile(context: QueryASTContext): OperationTranspileResult { + const clauses = this.partials.flatMap((partial) => { + return partial.transpile(context).clauses; + }); + return { + projectionExpr: context.returnVariable, + clauses, + }; + } + + getAuthorizationSubqueries(context: QueryASTContext): Clause[] { + return this.partials.flatMap((partial) => { + return partial.getAuthorizationSubqueries(context); + }); + } +} diff --git a/packages/graphql/src/translate/authorization/types/node-map.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectPartial.ts similarity index 78% rename from packages/graphql/src/translate/authorization/types/node-map.ts rename to packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectPartial.ts index 8836341e94..7277286d9a 100644 --- a/packages/graphql/src/translate/authorization/types/node-map.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeDisconnectPartial.ts @@ -17,11 +17,6 @@ * limitations under the License. */ -import type Cypher from "@neo4j/cypher-builder"; -import type { Node } from "../../../types"; +import { DisconnectOperation } from "../DisconnectOperation"; -export type NodeMap = { - node: Node; - variable: Cypher.Node; - fieldName?: string; -}; +export class CompositeDisconnectPartial extends DisconnectOperation {} diff --git a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/NodeSelectionPattern.ts b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/NodeSelectionPattern.ts index 54d924ca7c..e881e527f6 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/NodeSelectionPattern.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/NodeSelectionPattern.ts @@ -44,6 +44,10 @@ export class NodeSelectionPattern extends SelectionPattern { this.useContextTarget = useContextTarget; } + public print(): string { + return `${super.print()} <${this.target.name}>`; + } + public apply(context: QueryASTContext): { nestedContext: QueryASTContext; pattern: Cypher.Pattern; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/RelationshipSelectionPattern.ts b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/RelationshipSelectionPattern.ts index 1e800c7d8e..ccf7a0efa4 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/RelationshipSelectionPattern.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/RelationshipSelectionPattern.ts @@ -18,10 +18,11 @@ */ import Cypher from "@neo4j/cypher-builder"; +import type { EntityAdapter } from "../../../../../schema-model/entity/EntityAdapter"; import type { ConcreteEntityAdapter } from "../../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { hasTarget } from "../../../utils/context-has-target"; -import { createNode, getEntityLabels } from "../../../utils/create-node-from-entity"; +import { getEntityLabels } from "../../../utils/create-node-from-entity"; import type { QueryASTContext } from "../../QueryASTContext"; import { SelectionPattern } from "./SelectionPattern"; @@ -47,6 +48,10 @@ export class RelationshipSelectionPattern extends SelectionPattern { this.targetOverride = targetOverride; } + public print(): string { + return `${super.print()} <${this.relationship.name} -> ${this.target.name}>`; + } + public apply(context: QueryASTContext): { nestedContext: QueryASTContext; pattern: Cypher.Pattern; @@ -54,15 +59,14 @@ export class RelationshipSelectionPattern extends SelectionPattern { if (!hasTarget(context)) throw new Error("No parent node over a nested relationship match!"); const relVar = new Cypher.Relationship(); - const relationshipTarget = this.targetOverride ?? this.relationship.target; - const targetNode = createNode(this.alias); + const relationshipTarget = this.target; + const targetNode = new Cypher.Node(); const labels = getEntityLabels(relationshipTarget, context.neo4jGraphQLContext); const relDirection = this.relationship.getCypherDirection(); const pattern = new Cypher.Pattern(context.target) .related(relVar, { direction: relDirection, type: this.relationship.type }) .to(targetNode, { labels }); - // NOTE: Direction not passed (can we remove it from context?) const nestedContext = context.push({ target: targetNode, relationship: relVar }); @@ -71,4 +75,8 @@ export class RelationshipSelectionPattern extends SelectionPattern { pattern: pattern, }; } + + private get target(): EntityAdapter { + return this.targetOverride ?? this.relationship.target; + } } diff --git a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/SelectionPattern.ts b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/SelectionPattern.ts index 380cf0d350..db2a0cc05e 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/SelectionPattern.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/SelectionPattern/SelectionPattern.ts @@ -27,7 +27,8 @@ export abstract class SelectionPattern extends QueryASTNode { } /** Apply selection over the given context, returns the updated context and the selection clause - * TODO: Improve naming */ + * This ensures the new context matches the generated Cypher (i.e. the target is the nested relationship) + */ public abstract apply(context: QueryASTContext): { nestedContext: QueryASTContext; pattern: Cypher.Pattern; diff --git a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts index 079724d668..ee0b6df684 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -152,7 +152,7 @@ export class FilterFactory { } if (rel && key === "edge") { - return this.createEdgeFilters(rel, value); + return this.createEdgeFilters(rel, value ?? {}); } if (key === "node") { diff --git a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts index 436f87cca2..1d8cb90d58 100644 --- a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts @@ -32,9 +32,11 @@ import { filterTruthy, isRecord } from "../../../utils/utils"; import type { Filter } from "../ast/filters/Filter"; import type { AggregationOperation } from "../ast/operations/AggregationOperation"; import type { ConnectionReadOperation } from "../ast/operations/ConnectionReadOperation"; +import { type CreateOperation } from "../ast/operations/CreateOperation"; import type { CypherAttributeOperation } from "../ast/operations/CypherAttributeOperation"; import type { CypherEntityOperation } from "../ast/operations/CypherEntityOperation"; import type { ReadOperation } from "../ast/operations/ReadOperation"; +import { type UpdateOperation } from "../ast/operations/UpdateOperation"; import type { CompositeAggregationOperation } from "../ast/operations/composite/CompositeAggregationOperation"; import type { CompositeConnectionReadOperation } from "../ast/operations/composite/CompositeConnectionReadOperation"; import type { CompositeCypherOperation } from "../ast/operations/composite/CompositeCypherOperation"; @@ -55,6 +57,7 @@ import { ConnectionFactory } from "./Operations/ConnectionFactory"; import { CreateFactory } from "./Operations/CreateFactory"; import { CustomCypherFactory } from "./Operations/CustomCypherFactory"; import { DeleteFactory } from "./Operations/DeleteFactory"; +import { DisconnectFactory } from "./Operations/DisconnectFactory"; import { FulltextFactory } from "./Operations/FulltextFactory"; import { ReadFactory } from "./Operations/ReadFactory"; import { UpdateFactory } from "./Operations/UpdateFactory"; @@ -72,6 +75,7 @@ export class OperationsFactory { private authorizationFactory: AuthorizationFactory; private createFactory: CreateFactory; private connectFactory: ConnectFactory; + private disconnectFactory: DisconnectFactory; private updateFactory: UpdateFactory; private deleteFactory: DeleteFactory; private fulltextFactory: FulltextFactory; @@ -88,6 +92,7 @@ export class OperationsFactory { this.authorizationFactory = queryASTFactory.authorizationFactory; this.createFactory = new CreateFactory(queryASTFactory); this.connectFactory = new ConnectFactory(queryASTFactory); + this.disconnectFactory = new DisconnectFactory(queryASTFactory); this.updateFactory = new UpdateFactory(queryASTFactory); this.deleteFactory = new DeleteFactory(queryASTFactory); this.fulltextFactory = new FulltextFactory(queryASTFactory); @@ -203,7 +208,7 @@ export class OperationsFactory { } case "UPDATE": { assertIsConcreteEntity(entity); - return this.updateFactory.createUpdateOperation(entity, resolveTree, context); + return this.updateFactory.createUpdateOperation(entity, resolveTree, context, callbackBucket, varName); } case "DELETE": { assertIsConcreteEntity(entity); @@ -244,6 +249,68 @@ export class OperationsFactory { ); } } + public createDisconnectOperation( + entity: ConcreteEntityAdapter | InterfaceEntityAdapter | UnionEntityAdapter, + relationship: RelationshipAdapter, + input: Record[], + context: Neo4jGraphQLTranslationContext, + callbackBucket: CallbackBucket + ) { + if (isConcreteEntity(entity)) { + return this.disconnectFactory.createDisconnectOperation( + entity, + relationship, + input, + context, + callbackBucket + ); + } else { + return this.disconnectFactory.createCompositeDisconnectOperation( + entity, + relationship, + input, + context, + callbackBucket + ); + } + } + + public createNestedCreateOperation({ + relationship, + targetEntity, + input, + callbackBucket, + context, + operation, + key, + }: { + input: Record | Record[]; + targetEntity: ConcreteEntityAdapter | InterfaceEntityAdapter; + relationship: RelationshipAdapter; + callbackBucket: CallbackBucket; + context: Neo4jGraphQLTranslationContext; + operation: CreateOperation | UpdateOperation; + key: string; + }) { + return this.createFactory.createNestedCreateOperation({ + relationship, + targetEntity, + input, + callbackBucket, + context, + operation, + key, + }); + } + + public createNestedDeleteOperationsForUpdate( + deleteArg: Record, + relationship: RelationshipAdapter, + context: Neo4jGraphQLTranslationContext, + target: ConcreteEntityAdapter | InterfaceEntityAdapter + ) { + return this.deleteFactory.createNestedDeleteOperationsForUpdate(deleteArg, relationship, context, target); + } public createReadOperation(arg: { entityOrRel: EntityAdapter | RelationshipAdapter; diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts index d314df262c..27115546bc 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectFactory.ts @@ -34,6 +34,7 @@ import { NodeSelectionPattern } from "../../ast/selection/SelectionPattern/NodeS import type { CallbackBucket } from "../../utils/callback-bucket"; import { isConcreteEntity } from "../../utils/is-concrete-entity"; import { isInterfaceEntity } from "../../utils/is-interface-entity"; +import { isUnionEntity } from "../../utils/is-union-entity"; import { raiseAttributeAmbiguity } from "../../utils/raise-attribute-ambiguity"; import type { QueryASTFactory } from "../QueryASTFactory"; @@ -141,12 +142,19 @@ export class ConnectFactory { context, operation: connect, }); + if (isConcreteEntity(relationship.source)) { + this.addSourceEntityAuthorization({ + entity: relationship.source, + context, + operation: connect, + }); + } asArray(input).forEach((inputItem) => { const { whereArg, connectArg } = this.parseConnectArgs(inputItem); const nodeFilters: Filter[] = []; if (whereArg.node) { - if (isConcreteEntity(relationship.target)) { + if (isConcreteEntity(relationship.target) || isUnionEntity(relationship.target)) { nodeFilters.push(...this.queryASTFactory.filterFactory.createNodeFilters(target, whereArg.node)); } else if (isInterfaceEntity(relationship.target)) { nodeFilters.push( @@ -269,6 +277,25 @@ export class ConnectFactory { operation.addAuthFilters(...authFilters); } + private addSourceEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: ConnectOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["CREATE_RELATIONSHIP"], + context, + afterValidation: true, + }); + + operation.addSourceAuthFilters(...authFilters); + } + private getInputEdge(inputItem: Record, relationship: RelationshipAdapter): Record { const edge = inputItem.edge ?? {}; diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/CreateFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/CreateFactory.ts index 301ce52511..3d4df4b162 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/CreateFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/CreateFactory.ts @@ -33,6 +33,7 @@ import { CreateOperation } from "../../ast/operations/CreateOperation"; import type { ReadOperation } from "../../ast/operations/ReadOperation"; import { TopLevelCreateMutationOperation } from "../../ast/operations/TopLevelCreateMutationOperation"; import { UnwindCreateOperation } from "../../ast/operations/UnwindCreateOperation"; +import type { UpdateOperation } from "../../ast/operations/UpdateOperation"; import { NodeSelectionPattern } from "../../ast/selection/SelectionPattern/NodeSelectionPattern"; import { RelationshipSelectionPattern } from "../../ast/selection/SelectionPattern/RelationshipSelectionPattern"; import type { CallbackBucket } from "../../utils/callback-bucket"; @@ -392,7 +393,7 @@ export class CreateFactory { }); } - private createNestedCreateOperation({ + public createNestedCreateOperation({ relationship, targetEntity, input, @@ -406,7 +407,7 @@ export class CreateFactory { relationship: RelationshipAdapter; callbackBucket: CallbackBucket; context: Neo4jGraphQLTranslationContext; - operation: CreateOperation; + operation: CreateOperation | UpdateOperation; key: string; }) { asArray(input).forEach((input) => { diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/DeleteFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/DeleteFactory.ts index 11b02d2a1b..37aaf04846 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/DeleteFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/DeleteFactory.ts @@ -183,6 +183,29 @@ export class DeleteFactory { ); } + public createNestedDeleteOperationsForUpdate( + deleteArg: Record, + relationship: RelationshipAdapter, + context: Neo4jGraphQLTranslationContext, + target: ConcreteEntityAdapter | InterfaceEntityAdapter + ): DeleteOperation[] { + if (isInterfaceEntity(target)) { + return this.createNestedDeleteOperationsForInterface({ + deleteArg, + relationship, + target, + context, + }); + } + + return this.createNestedDeleteOperation({ + relationship, + target, + args: deleteArg, + context, + }); + } + private createNestedDeleteOperation({ relationship, target, diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts new file mode 100644 index 0000000000..7c32625556 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/factory/Operations/DisconnectFactory.ts @@ -0,0 +1,282 @@ +/* + * 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 { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { UnionEntityAdapter } from "../../../../schema-model/entity/model-adapters/UnionEntityAdapter"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import type { Neo4jGraphQLTranslationContext } from "../../../../types/neo4j-graphql-translation-context"; +import { asArray } from "../../../../utils/utils"; +import type { Filter } from "../../ast/filters/Filter"; +import { MutationOperationField } from "../../ast/input-fields/MutationOperationField"; +import { ParamInputField } from "../../ast/input-fields/ParamInputField"; +import { DisconnectOperation } from "../../ast/operations/DisconnectOperation"; +import { CompositeDisconnectOperation } from "../../ast/operations/composite/CompositeDisconnectOperation"; +import { CompositeDisconnectPartial } from "../../ast/operations/composite/CompositeDisconnectPartial"; +import { RelationshipSelectionPattern } from "../../ast/selection/SelectionPattern/RelationshipSelectionPattern"; +import type { CallbackBucket } from "../../utils/callback-bucket"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; +import { isInterfaceEntity } from "../../utils/is-interface-entity"; +import { isUnionEntity } from "../../utils/is-union-entity"; +import { raiseAttributeAmbiguity } from "../../utils/raise-attribute-ambiguity"; +import type { QueryASTFactory } from "../QueryASTFactory"; + +export class DisconnectFactory { + private queryASTFactory: QueryASTFactory; + + constructor(queryASTFactory: QueryASTFactory) { + this.queryASTFactory = queryASTFactory; + } + + public createDisconnectOperation( + entity: ConcreteEntityAdapter, + relationship: RelationshipAdapter, + input: Record[], + context: Neo4jGraphQLTranslationContext, + callbackBucket: CallbackBucket + ): DisconnectOperation { + const disconnectOP = new DisconnectOperation({ + target: entity, + selectionPattern: new RelationshipSelectionPattern({ + relationship, + targetOverride: entity, + }), + relationship, + }); + + this.hydrateDisconnectOperation({ + target: entity, + relationship, + input, + disconnect: disconnectOP, + context, + callbackBucket, + }); + return disconnectOP; + } + + public createCompositeDisconnectOperation( + entity: InterfaceEntityAdapter | UnionEntityAdapter, + relationship: RelationshipAdapter, + input: Record[], + context: Neo4jGraphQLTranslationContext, + callbackBucket: CallbackBucket + ): CompositeDisconnectOperation { + const partials: CompositeDisconnectPartial[] = []; + for (const concreteEntity of entity.concreteEntities) { + const partial = this.createCompositeDisconnectPartial( + concreteEntity, + relationship, + input, + context, + callbackBucket + ); + partials.push(partial); + } + + return new CompositeDisconnectOperation({ + partials, + target: entity, + }); + } + + private createCompositeDisconnectPartial( + entity: ConcreteEntityAdapter, + relationship: RelationshipAdapter, + input: Record[], + context: Neo4jGraphQLTranslationContext, + callbackBucket: CallbackBucket + ): CompositeDisconnectPartial { + const disconnectOp = new CompositeDisconnectPartial({ + target: entity, + selectionPattern: new RelationshipSelectionPattern({ + relationship, + targetOverride: entity, + }), + relationship, + }); + + this.hydrateDisconnectOperation({ + target: entity, + relationship, + input, + disconnect: disconnectOp, + context, + callbackBucket, + }); + return disconnectOp; + } + + private hydrateDisconnectOperation({ + target, + relationship, + input, + disconnect, + context, + callbackBucket, + }: { + target: ConcreteEntityAdapter; + relationship: RelationshipAdapter; + input: Record[]; + disconnect: DisconnectOperation; + context: Neo4jGraphQLTranslationContext; + callbackBucket: CallbackBucket; + }) { + this.addEntityAuthorization({ + entity: target, + context, + operation: disconnect, + }); + + if (isConcreteEntity(relationship.source)) { + this.addSourceEntityAuthorization({ + entity: relationship.source, + context, + operation: disconnect, + }); + } + + asArray(input).forEach((inputItem) => { + const { whereArg, disconnectArg } = this.parseDisconnectArgs(inputItem); + const nodeFilters: Filter[] = []; + const edgeFilters: Filter[] = []; + if (whereArg.node) { + if (isConcreteEntity(relationship.target) || isUnionEntity(relationship.target)) { + nodeFilters.push(...this.queryASTFactory.filterFactory.createNodeFilters(target, whereArg.node)); + } else if (isInterfaceEntity(relationship.target)) { + nodeFilters.push( + ...this.queryASTFactory.filterFactory.createInterfaceNodeFilters({ + entity: relationship.target, + targetEntity: target, + whereFields: whereArg.node, + relationship, + }) + ); + } + } + if (whereArg.edge) { + edgeFilters.push(...this.queryASTFactory.filterFactory.createEdgeFilters(relationship, whereArg.edge)); + } + + disconnect.addFilters(...nodeFilters, ...edgeFilters); + + asArray(disconnectArg).forEach((nestedDisconnectInputFields) => { + Object.entries(nestedDisconnectInputFields).forEach(([key, value]) => { + const nestedRelationship = target.relationships.get(key); + if (!nestedRelationship) { + throw new Error("Expected relationship on connect operation. Please contact support"); + } + + const nestedEntity = nestedRelationship.target; + + asArray(value).forEach((nestedDisconnectInputItem) => { + const nestedDisconnectOperation = + this.queryASTFactory.operationsFactory.createDisconnectOperation( + nestedEntity, + nestedRelationship, + nestedDisconnectInputItem, + context, + callbackBucket + ); + + const mutationOperationField = new MutationOperationField(nestedDisconnectOperation, key); + disconnect.addField(mutationOperationField, "node"); + }); + }); + }); + + const targetInputEdge = this.getInputEdge(inputItem, relationship); + + /* Create the attributes for the edge */ + raiseAttributeAmbiguity(Object.keys(targetInputEdge), relationship); + for (const key of Object.keys(targetInputEdge)) { + const attribute = relationship.attributes.get(key); + if (attribute) { + const attachedTo = "relationship"; + + const paramInputField = new ParamInputField({ + attachedTo, + attribute, + inputValue: targetInputEdge[key], + }); + disconnect.addField(paramInputField, attachedTo); + } + } + }); + } + + private addEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: DisconnectOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["DELETE_RELATIONSHIP"], + context, + afterValidation: true, + }); + + operation.addAuthFilters(...authFilters); + } + private addSourceEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: DisconnectOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["DELETE_RELATIONSHIP"], + context, + afterValidation: true, + }); + + operation.addSourceAuthFilters(...authFilters); + } + + private getInputEdge(inputItem: Record, relationship: RelationshipAdapter): Record { + const edge = inputItem.edge ?? {}; + + // Deals with composite relationships + if (relationship.propertiesTypeName && edge[relationship.propertiesTypeName]) { + return edge[relationship.propertiesTypeName]; + } + + return edge; + } + + private parseDisconnectArgs(args: Record): { + whereArg: { node: Record; edge: Record }; + disconnectArg: Record[]; + } { + const rawWhere = args.where ?? {}; + + const whereArg = { node: rawWhere.node, edge: rawWhere.edge }; + const disconnectArg = args.disconnect ?? {}; + return { whereArg, disconnectArg }; + } +} diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts index 4f3e8d55ea..9761030af4 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/UpdateFactory.ts @@ -17,11 +17,34 @@ * limitations under the License. */ +import Cypher from "@neo4j/cypher-builder"; +import { GraphQLError } from "graphql"; import type { ResolveTree } from "graphql-parse-resolve-info"; +import type { AttributeAdapter } from "../../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import type { Neo4jGraphQLTranslationContext } from "../../../../types/neo4j-graphql-translation-context"; +import { asArray } from "../../../../utils/utils"; +import { OperationField } from "../../ast/fields/OperationField"; +import { type InputField } from "../../ast/input-fields/InputField"; +import { MutationOperationField } from "../../ast/input-fields/MutationOperationField"; +import { MathInputField } from "../../ast/input-fields/operators/MathInputField"; +import { PopInputField } from "../../ast/input-fields/operators/PopInputField"; +import { PushInputField } from "../../ast/input-fields/operators/PushInputField"; +import { ParamInputField } from "../../ast/input-fields/ParamInputField"; import type { ReadOperation } from "../../ast/operations/ReadOperation"; +import { TopLevelUpdateMutationOperation } from "../../ast/operations/TopLevelUpdateMutationOperation"; import { UpdateOperation } from "../../ast/operations/UpdateOperation"; +import { NodeSelectionPattern } from "../../ast/selection/SelectionPattern/NodeSelectionPattern"; +import { RelationshipSelectionPattern } from "../../ast/selection/SelectionPattern/RelationshipSelectionPattern"; +import type { CallbackBucket } from "../../utils/callback-bucket"; +import { isConcreteEntity } from "../../utils/is-concrete-entity"; +import { isUnionEntity } from "../../utils/is-union-entity"; +import { raiseAttributeAmbiguityForUpdate } from "../../utils/raise-attribute-ambiguity"; +import { getAutogeneratedFieldsForUpdate } from "../parsers/get-autogenerated-fields"; +import type { MutationOperator } from "../parsers/parse-mutation-field"; +import { parseMutationField } from "../parsers/parse-mutation-field"; import type { QueryASTFactory } from "../QueryASTFactory"; export class UpdateFactory { @@ -34,13 +57,46 @@ export class UpdateFactory { public createUpdateOperation( entity: ConcreteEntityAdapter, resolveTree: ResolveTree, - context: Neo4jGraphQLTranslationContext - ): UpdateOperation { + context: Neo4jGraphQLTranslationContext, + callbackBucket: CallbackBucket, + varName: string | undefined + ): TopLevelUpdateMutationOperation { + const rawInput = resolveTree.args.update as Record[]; + const input = asArray(rawInput) ?? []; + + if (!input.length) { + // dummy input to translate top level match for the projection to work + input.push({}); + } + + const updateOperations = input.map((inputItem) => { + const updateOperation = new UpdateOperation({ + target: entity, + selectionPattern: new NodeSelectionPattern({ + target: entity, + alias: varName, + }), + }); + + this.hydrateUpdateOperation({ + target: entity, + input: inputItem, + update: updateOperation, + callbackBucket, + context, + whereArgs: { + node: (resolveTree.args.where as Record) ?? {}, + }, + }); + + return updateOperation; + }); + const responseFields = Object.values( resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.update] ?? {} ); - const updateOp = new UpdateOperation({ target: entity }); - const projectionFields = responseFields + + const projectionOperations = responseFields .filter((f) => f.name === entity.plural) .map((field) => { const readOP = this.queryASTFactory.operationsFactory.createReadOperation({ @@ -48,10 +104,654 @@ export class UpdateFactory { resolveTree: field, context, }) as ReadOperation; - return readOP; + + const fieldOperation = new OperationField({ + operation: readOP, + alias: field.alias, + }); + return fieldOperation; }); + const topLevelMutation = new TopLevelUpdateMutationOperation({ + updateOperations, + projectionOperations, + }); + return topLevelMutation; + } + + private hydrateUpdateOperation({ + target, + relationship, + input, + update, + callbackBucket, + context, + whereArgs, + }: { + target: ConcreteEntityAdapter; + relationship?: RelationshipAdapter; + input: Record; + update: UpdateOperation; + callbackBucket: CallbackBucket; + context: Neo4jGraphQLTranslationContext; + whereArgs: { + node: Record; + edge?: Record; + }; + }) { + const isNested = Boolean(relationship); + + const autoGeneratedFields = getAutogeneratedFieldsForUpdate(target); + autoGeneratedFields.forEach((field) => { + update.addField(field); + }); + + if (relationship) { + const autoGeneratedFields = getAutogeneratedFieldsForUpdate(relationship); + autoGeneratedFields.forEach((field) => { + field.attachedTo = "relationship"; + update.addField(field); + }); + } + + if (this.shouldApplyUpdateAuthorization(input, target, isNested)) { + this.addEntityAuthorization({ entity: target, context, operation: update }); + } + asArray(input).forEach((inputItem) => { + const targetInput = this.getInputNode(inputItem, isNested); + raiseAttributeAmbiguityForUpdate(Object.keys(targetInput), target); + raiseAttributeAmbiguityForUpdate(Object.keys(this.getInputEdge(inputItem)), relationship); + + if (whereArgs) { + const filters = this.queryASTFactory.filterFactory.createConnectionPredicates({ + rel: relationship, + entity: target, + where: whereArgs, + }); + update.addFilters(...filters); + } + for (const key of Object.keys(targetInput)) { + const { fieldName, operator } = parseMutationField(key); + const nestedRelationship = target.relationships.get(fieldName); + const attribute = target.attributes.get(fieldName); + if (!attribute && !nestedRelationship) { + throw new Error(`Transpile Error: Input field ${key} not found in entity ${target.name}`); + } + if (attribute) { + if (operator) { + const value = targetInput[key]; + if (attribute.typeHelper.isRequired() && value === null && operator === "SET") { + throw new Error(`Cannot set non-nullable field ${target.name}.${attribute.name} to null`); + } + const paramInputField = this.getInputFieldDeprecated("node", operator, attribute, value); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } else { + const operations = Object.keys(targetInput[fieldName]); + if (operations.length > 1) { + const conflictingOperations = operations.map((op) => `[[${op}]]`); + throw new GraphQLError( + `Conflicting modification of field ${fieldName}: ${conflictingOperations.join(", ")} on type ${target.name}` + ); + } + for (const op of Object.keys(targetInput[fieldName])) { + const value = targetInput[fieldName][op]; + if (attribute.typeHelper.isRequired() && value === null && op === "set") { + throw new Error( + `Cannot set non-nullable field ${target.name}.${attribute.name} to null` + ); + } + const paramInputField = this.getInputField("node", op, attribute, value); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } + } + } else if (nestedRelationship) { + const nestedEntity = nestedRelationship.target; + const operationInput = targetInput[key] ?? {}; - updateOp.addProjectionOperations(projectionFields); - return updateOp; + const entityAndNodeInput: Array< + [ConcreteEntityAdapter | InterfaceEntityAdapter, Record] + > = []; + + if (isUnionEntity(nestedEntity)) { + Object.entries(operationInput).forEach(([entityTypename, input]) => { + const concreteNestedEntity = nestedEntity.concreteEntities.find( + (e) => e.name === entityTypename + ); + if (!concreteNestedEntity) { + throw new Error("Concrete entity not found in create, please contact support"); + } + + entityAndNodeInput.push([concreteNestedEntity, input as any]); + }); + } else { + entityAndNodeInput.push([nestedEntity, operationInput]); + } + + entityAndNodeInput.forEach(([nestedEntity, operations]) => { + operations.forEach((operationInput: Record) => { + const nestedUpdateInput = operationInput.update; + if (nestedUpdateInput) { + asArray(nestedUpdateInput).forEach((nestedUpdateInputItem) => { + this.createNestedUpdateOperation({ + nestedEntity, + nestedRelationship, + nestedUpdateInputItem, + context, + callbackBucket, + operation: update, + key, + }); + }); + } + const nestedCreateInput = operationInput.create; + if (nestedCreateInput) { + asArray(nestedCreateInput).forEach((nestedCreateInputItem) => { + let edgeField = nestedCreateInputItem.edge ?? {}; + + // This is to parse the create input for a declareRelationship + // We are checking the relationship target, because for nestedRelationship is + // already disambiguated into concrete entity + if (relationship?.target && !isConcreteEntity(relationship?.target)) { + if (nestedRelationship.propertiesTypeName) { + edgeField = edgeField[nestedRelationship.propertiesTypeName] ?? {}; + } + } + + const concreteNestedCreateInput = { + node: nestedCreateInputItem.node ?? {}, + edge: edgeField, + }; + + this.queryASTFactory.operationsFactory.createNestedCreateOperation({ + targetEntity: nestedEntity, + relationship: nestedRelationship, + input: concreteNestedCreateInput, + context, + callbackBucket, + key, + operation: update, + }); + }); + } + const nestedConnectInput = operationInput.connect; + if (nestedConnectInput) { + asArray(nestedConnectInput).forEach((nestedConnectInputItem) => { + const nestedConnectOperation = + this.queryASTFactory.operationsFactory.createConnectOperation( + nestedEntity, + nestedRelationship, + nestedConnectInputItem, + context, + callbackBucket + ); + + const mutationOperationField = new MutationOperationField( + nestedConnectOperation, + key + ); + update.addField(mutationOperationField); + }); + } + const nestedDeleteInput = operationInput.delete; + if (nestedDeleteInput) { + asArray(nestedDeleteInput).forEach((nestedDeleteInputItem) => { + const nestedDeleteOperations = + this.queryASTFactory.operationsFactory.createNestedDeleteOperationsForUpdate( + nestedDeleteInputItem, + nestedRelationship, + context, + nestedEntity + ); + for (const nestedDeleteOperation of nestedDeleteOperations) { + const mutationOperationField = new MutationOperationField( + nestedDeleteOperation, + key + ); + update.addField(mutationOperationField); + } + }); + } + const nestedDisconnectInput = operationInput.disconnect; + if (nestedDisconnectInput) { + asArray(nestedDisconnectInput).forEach((nestedDisconnectInputItem) => { + const nestedDisconnectOperation = + this.queryASTFactory.operationsFactory.createDisconnectOperation( + nestedEntity, + nestedRelationship, + nestedDisconnectInputItem, + context, + callbackBucket + ); + + const mutationOperationField = new MutationOperationField( + nestedDisconnectOperation, + key + ); + update.addField(mutationOperationField); + }); + } + }); + }); + } + } + + if (relationship) { + const targetInputEdge = this.getInputEdge(inputItem); + for (const key of Object.keys(targetInputEdge)) { + const { fieldName, operator } = parseMutationField(key); + const attribute = relationship.attributes.get(fieldName); + if (attribute) { + if (operator) { + const paramInputField = this.getInputFieldDeprecated( + "relationship", + operator, + attribute, + targetInputEdge[key] + ); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } else { + const operations = Object.keys(targetInputEdge[fieldName]); + if (operations.length > 1) { + const conflictingOperations = operations.map((op) => `[[${op}]]`); + throw new GraphQLError( + `Conflicting modification of field ${fieldName}: ${conflictingOperations.join(", ")} on relationship ${target.name}.${relationship.name}` + ); + } + for (const op of operations) { + const paramInputField = this.getInputField( + "relationship", + op, + attribute, + targetInputEdge[fieldName][op] + ); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } + } + } else if (key === relationship.propertiesTypeName) { + const edgeInput = targetInputEdge[key]; // ActedIn: {..} + for (const k of Object.keys(edgeInput)) { + const { fieldName, operator } = parseMutationField(k); + const attribute = relationship.attributes.get(fieldName); + if (attribute) { + if (operator) { + const paramInputField = this.getInputFieldDeprecated( + "relationship", + operator, + attribute, + edgeInput[k] + ); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } else { + for (const op of Object.keys(edgeInput[k][fieldName])) { + const paramInputField = this.getInputField( + "relationship", + op, + attribute, + edgeInput[fieldName][op] + ); + update.addField(paramInputField); + + this.addAttributeAuthorization({ + attribute, + context, + update, + entity: target, + }); + } + } + } + } + } + } + if (Object.keys(targetInputEdge).length > 0) { + this.addPopulatedByFieldToUpdate({ + entity: target, + update, + input: targetInputEdge, + callbackBucket, + relationship, + }); + } + } + this.addPopulatedByFieldToUpdate({ + entity: target, + update, + input: targetInput, + callbackBucket, + }); + }); + } + + private addPopulatedByFieldToUpdate({ + entity, + update, + input, + callbackBucket, + relationship, + }: { + entity: ConcreteEntityAdapter; + update: UpdateOperation; + input: Record; + callbackBucket: CallbackBucket; + relationship?: RelationshipAdapter; + }) { + entity.getPopulatedByFields("UPDATE").forEach((attribute) => { + const attachedTo = "node"; + // the param value it's irrelevant as it will be overwritten by the callback function + const callbackParam = new Cypher.Param(""); + const field = new ParamInputField({ + attribute, + attachedTo, + inputValue: callbackParam, + }); + update.addField(field); + + const callbackFunctionName = attribute.annotations.populatedBy?.callback; + if (!callbackFunctionName) { + throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`); + } + + const callbackParent = relationship ? input.node : input; + + callbackBucket.addCallback({ + functionName: callbackFunctionName, + param: callbackParam, + parent: callbackParent, + type: attribute.type, + }); + }); + + if (relationship) { + relationship.getPopulatedByFields("UPDATE").forEach((attribute) => { + const attachedTo = "relationship"; + // the param value it's irrelevant as it will be overwritten by the callback function + const relCallbackParam = new Cypher.Param(""); + const relField = new ParamInputField({ + attribute, + attachedTo, + inputValue: relCallbackParam, + }); + update.addField(relField); + + const callbackFunctionName = attribute.annotations.populatedBy?.callback; + if (!callbackFunctionName) { + throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`); + } + + callbackBucket.addCallback({ + functionName: callbackFunctionName, + param: relCallbackParam, + parent: input, + type: attribute.type, + }); + }); + } + } + + private addEntityAuthorization({ + entity, + context, + operation, + }: { + entity: ConcreteEntityAdapter; + context: Neo4jGraphQLTranslationContext; + operation: UpdateOperation; + }): void { + const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ + entity, + operations: ["UPDATE"], + context, + afterValidation: true, + }); + + operation.addAuthFilters(...authFilters); + } + + private addAttributeAuthorization({ + attribute, + context, + update, + entity, + conditionForEvaluation, + }: { + attribute: AttributeAdapter; + context: Neo4jGraphQLTranslationContext; + update: UpdateOperation; + entity: ConcreteEntityAdapter; + conditionForEvaluation?: Cypher.Predicate; + }): void { + const authBeforeFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + entity, + authAnnotation: attribute.annotations.authorization, + when: "BEFORE", + conditionForEvaluation, + operations: ["UPDATE"], + context, + }); + if (authBeforeFilters) { + update.addAuthFilters(authBeforeFilters); + } + const attributeAuthorization = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ + entity, + when: "AFTER", + authAnnotation: attribute.annotations.authorization, + conditionForEvaluation, + operations: ["UPDATE"], + context, + }); + if (attributeAuthorization) { + update.addAuthFilters(attributeAuthorization); + } + } + + // returns true only if actual attributes are modified + // UPDATE rules should not be applied for (dis)connections + private shouldApplyUpdateAuthorization( + input: Record, + entity: ConcreteEntityAdapter, + isNested: boolean + ): boolean { + const actualInput = this.getInputNode(input, isNested); + const affectedKeys = Object.keys(actualInput).map((key) => { + // old version compatibility (eg id_SET) + const { fieldName } = parseMutationField(key); + return fieldName; + }); + const areAttributesAffected = affectedKeys.filter((k) => entity.attributes.has(k)).length > 0; + + const isRelationshipUpdated = + isNested && + affectedKeys + .filter((k) => entity.relationships.has(k)) + .some((k) => { + return asArray(actualInput[k]).filter((inputItem) => inputItem.update); + }); + + return areAttributesAffected || isRelationshipUpdated; + } + + private getInputNode(inputItem: Record, isNested: boolean): Record { + if (isNested) { + return inputItem.node ?? {}; + } + return inputItem; + } + + private getInputEdge(inputItem: Record): Record { + return inputItem.edge ?? {}; + } + + private getInputFieldDeprecated( + attachedTo: "node" | "relationship", + operator: MutationOperator | undefined, + attribute: AttributeAdapter, + value: unknown + ): InputField { + switch (operator) { + case "SET": + return new ParamInputField({ + attachedTo, + attribute, + inputValue: value, + }); + case "INCREMENT": + case "DECREMENT": + case "ADD": + case "SUBTRACT": + case "DIVIDE": + case "MULTIPLY": + return new MathInputField({ + attachedTo, + attribute, + inputValue: value, + operation: operator.toLowerCase() as any, + }); + case "PUSH": + return new PushInputField({ + attachedTo, + attribute, + inputValue: value, + }); + case "POP": + return new PopInputField({ + attachedTo, + attribute, + inputValue: value, + }); + default: + throw new Error(`Unsupported update operator ${operator} on field ${attribute.name} `); + } + } + private getInputField( + attachedTo: "node" | "relationship", + operator: string | undefined, + attribute: AttributeAdapter, + value: unknown + ): InputField { + switch (operator) { + case "set": + return new ParamInputField({ + attachedTo: "node", + attribute, + inputValue: value, + }); + case "increment": + case "decrement": + case "add": + case "subtract": + case "divide": + case "multiply": + return new MathInputField({ + attachedTo, + attribute, + inputValue: value, + operation: operator, + }); + case "push": + return new PushInputField({ + attachedTo, + attribute, + inputValue: value, + }); + case "pop": + return new PopInputField({ + attachedTo, + attribute, + inputValue: value, + }); + default: + throw new Error(`Unsupported update operator ${operator} on field ${attribute.name} `); + } + } + + private createNestedUpdateOperation({ + nestedEntity, + nestedRelationship, + nestedUpdateInputItem, + context, + callbackBucket, + operation, + key, + }: { + nestedEntity: ConcreteEntityAdapter | InterfaceEntityAdapter; + nestedRelationship: RelationshipAdapter; + nestedUpdateInputItem: Record; + context: Neo4jGraphQLTranslationContext; + callbackBucket: CallbackBucket; + operation: UpdateOperation; + key: string; + }) { + asArray(nestedUpdateInputItem).forEach((input) => { + const edgeFields = input.edge ?? {}; + const nodeInputFields = input.node ?? {}; + + const entityAndNodeInput: Array<[ConcreteEntityAdapter, Record]> = []; + + if (isConcreteEntity(nestedEntity)) { + entityAndNodeInput.push([nestedEntity, nodeInputFields]); + } else { + nestedEntity.concreteEntities.forEach((concreteEntity) => { + entityAndNodeInput.push([concreteEntity, nodeInputFields]); + }); + } + + entityAndNodeInput.forEach(([concreteEntity, nodeInputFields]) => { + const nestedUpdateOperation = new UpdateOperation({ + target: concreteEntity, + relationship: nestedRelationship, + selectionPattern: new RelationshipSelectionPattern({ + relationship: nestedRelationship, + targetOverride: concreteEntity, + }), + }); + + this.hydrateUpdateOperation({ + target: concreteEntity, + relationship: nestedRelationship, + input: { node: nodeInputFields, edge: edgeFields }, + update: nestedUpdateOperation, + callbackBucket, + context, + whereArgs: input.where ?? {}, + }); + + const mutationOperationField = new MutationOperationField(nestedUpdateOperation, key); + operation.addField(mutationOperationField); + }); + }); } } diff --git a/packages/graphql/src/translate/queryAST/factory/parsers/get-autogenerated-fields.ts b/packages/graphql/src/translate/queryAST/factory/parsers/get-autogenerated-fields.ts index 150f5ae436..0e57428c0a 100644 --- a/packages/graphql/src/translate/queryAST/factory/parsers/get-autogenerated-fields.ts +++ b/packages/graphql/src/translate/queryAST/factory/parsers/get-autogenerated-fields.ts @@ -38,3 +38,16 @@ export function getAutogeneratedFields( } return autoGeneratedFields; } + +export function getAutogeneratedFieldsForUpdate( + target: ConcreteEntityAdapter | RelationshipAdapter +): (IdField | TimestampField)[] { + const attachedTo = isConcreteEntity(target) ? "node" : "relationship"; + const autoGeneratedFields: (IdField | TimestampField)[] = []; + for (const attribute of target.attributes.values()) { + if (attribute.timestampUpdateIsGenerated()) { + autoGeneratedFields.push(new TimestampField(attribute.name, attribute, attachedTo)); + } + } + return autoGeneratedFields; +} diff --git a/packages/graphql/src/translate/queryAST/utils/assert-is-cypher-node.ts b/packages/graphql/src/translate/queryAST/utils/assert-is-cypher-node.ts deleted file mode 100644 index a09e6755e5..0000000000 --- a/packages/graphql/src/translate/queryAST/utils/assert-is-cypher-node.ts +++ /dev/null @@ -1,31 +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 Cypher from "@neo4j/cypher-builder"; - -/** Checks if provided variable is a Cypher.Node instance */ -function isCypherNode(variable: Cypher.Variable): variable is Cypher.Node { - return variable instanceof Cypher.Node; -} -/** Asserts the given variable is a Cypher.Node instance */ -export function assertIsCypherNode(variable: Cypher.Variable): asserts variable is Cypher.Node { - if (!isCypherNode(variable)) { - throw new Error("Compile Error: Expected Cypher.Variable to be a Cypher.Node"); - } -} diff --git a/packages/graphql/src/translate/queryAST/utils/callback-bucket.ts b/packages/graphql/src/translate/queryAST/utils/callback-bucket.ts index 01ecb8c69a..0812ff8256 100644 --- a/packages/graphql/src/translate/queryAST/utils/callback-bucket.ts +++ b/packages/graphql/src/translate/queryAST/utils/callback-bucket.ts @@ -79,7 +79,13 @@ export class CallbackBucket { const callbackFunction = callbacksList[cb.functionName]; if (callbackFunction) { const paramValue = await callbackFunction(cb.parent, {}, this.context); - cb.param.value = this.parseCallbackResult(paramValue, cb.type); + if (paramValue === undefined) { + cb.param.value = undefined; + } else if (paramValue === null) { + cb.param.value = null; + } else { + cb.param.value = this.parseCallbackResult(paramValue, cb.type); + } } }) ); diff --git a/packages/graphql/src/translate/queryAST/utils/is-composite-entity.ts b/packages/graphql/src/translate/queryAST/utils/is-composite-entity.ts deleted file mode 100644 index 2cc22c974a..0000000000 --- a/packages/graphql/src/translate/queryAST/utils/is-composite-entity.ts +++ /dev/null @@ -1,29 +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 { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; -import { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import { UnionEntityAdapter } from "../../../schema-model/entity/model-adapters/UnionEntityAdapter"; -import type { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; - -export function isCompositeEntity( - entity: EntityAdapter | RelationshipAdapter -): entity is InterfaceEntityAdapter | UnionEntityAdapter { - return entity instanceof InterfaceEntityAdapter || entity instanceof UnionEntityAdapter; -} diff --git a/packages/graphql/src/translate/queryAST/utils/raise-attribute-ambiguity.ts b/packages/graphql/src/translate/queryAST/utils/raise-attribute-ambiguity.ts index 571a7891bf..7e59795570 100644 --- a/packages/graphql/src/translate/queryAST/utils/raise-attribute-ambiguity.ts +++ b/packages/graphql/src/translate/queryAST/utils/raise-attribute-ambiguity.ts @@ -19,7 +19,8 @@ import { Neo4jGraphQLError } from "../../../classes"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { findConflictingAttributes } from "../../../utils/find-conflicting-properties"; import { isConcreteEntity } from "./is-concrete-entity"; // Schema Model version of findConflictingProperties @@ -51,3 +52,27 @@ export function raiseAttributeAmbiguity( hash[dbName] = property; }); } + +// Schema Model version of assertNonAmbiguousUpdate +export function raiseAttributeAmbiguityForUpdate( + properties: Array, + entityOrRel?: ConcreteEntityAdapter | RelationshipAdapter +): void { + if (!entityOrRel) { + return; + } + + const conflictingAttributes = findConflictingAttributes(properties, entityOrRel); + if (conflictingAttributes.size > 0) { + const conflictingAttributesString = Array.from(conflictingAttributes).map((attribute) => `[[${attribute}]]`); + //This will only throw on the first conflicting attribute through + + const typeName = + entityOrRel instanceof RelationshipAdapter + ? `${entityOrRel.source.name}.${entityOrRel.name}` + : `${entityOrRel.name}`; + throw new Neo4jGraphQLError( + `Conflicting modification of ${conflictingAttributesString.join(", ")} on type ${typeName}` + ); + } +} diff --git a/packages/graphql/src/translate/queryAST/utils/raise-on-mixed-pagination.ts b/packages/graphql/src/translate/queryAST/utils/raise-on-mixed-pagination.ts deleted file mode 100644 index 67af16fd8a..0000000000 --- a/packages/graphql/src/translate/queryAST/utils/raise-on-mixed-pagination.ts +++ /dev/null @@ -1,36 +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. - */ - -export function raiseOnMixedPagination({ - optionsArg, - sort, - limit, - offset, -}: { - optionsArg: Record; - sort: any; - limit: any; - offset: any; -}) { - if (Object.keys(optionsArg).length > 0 && (sort !== undefined || limit !== undefined || offset !== undefined)) { - throw new Error( - `Ambiguous pagination found. The options argument is deprecated. Please use the sort, limit, and offset arguments directly on the field.` - ); - } -} diff --git a/packages/graphql/src/translate/queryAST/utils/wrap-subquery-in-call.ts b/packages/graphql/src/translate/queryAST/utils/wrap-subquery-in-call.ts deleted file mode 100644 index 67e5d838e7..0000000000 --- a/packages/graphql/src/translate/queryAST/utils/wrap-subquery-in-call.ts +++ /dev/null @@ -1,25 +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 Cypher from "@neo4j/cypher-builder"; - -/** Wraps provided queries in Call statements with inner target */ -export function wrapSubqueryInCall(subquery: Cypher.Clause, target: Cypher.Variable): Cypher.Call { - return new Cypher.Call(subquery, [target]); -} diff --git a/packages/graphql/src/translate/subscriptions/create-connection-event-meta.ts b/packages/graphql/src/translate/subscriptions/create-connection-event-meta.ts deleted file mode 100644 index a76f9a2a19..0000000000 --- a/packages/graphql/src/translate/subscriptions/create-connection-event-meta.ts +++ /dev/null @@ -1,80 +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 { META_CYPHER_VARIABLE } from "../../constants"; - -type SubscriptionsEventType = "create_relationship" | "delete_relationship"; - -type EventMetaParameters = EventMetaTypenameParameters | EventMetaLabelsParameters; -type EventMetaCommonParameters = { - event: SubscriptionsEventType; - relVariable: string; - typename: string; - fromVariable: string; - toVariable: string; -}; -type EventMetaTypenameParameters = EventMetaCommonParameters & { - fromTypename: string; - toTypename: string; -}; -type EventMetaLabelsParameters = EventMetaCommonParameters & { - fromLabels: string; - toLabels: string; - toProperties: string; - fromProperties: string; -}; - -function isEventMetaWithTypenames(event: EventMetaParameters): event is EventMetaTypenameParameters { - return !!event["fromTypename"]; -} - -function projectAllProperties(varName: string): string { - return `${varName} { .* }`; -} - -export function createConnectionEventMeta(params: EventMetaParameters): string { - return `${META_CYPHER_VARIABLE} + ${createConnectionEventMetaObject(params)} AS ${META_CYPHER_VARIABLE}`; -} - -export function createConnectionEventMetaObject(eventMeta: EventMetaParameters): string { - const { event, relVariable, typename, fromVariable, toVariable } = eventMeta; - - const commonFieldsStr = `event: "${event}", timestamp: timestamp()`; - const identifiersStr = `id_from: id(${fromVariable}), id_to: id(${toVariable}), id: id(${relVariable})`; - - if (isEventMetaWithTypenames(eventMeta)) { - return `{ ${[ - commonFieldsStr, - identifiersStr, - `relationshipName: "${typename}", fromTypename: "${eventMeta.fromTypename}", toTypename: "${eventMeta.toTypename}"`, - `properties: { from: ${projectAllProperties(fromVariable)}, to: ${projectAllProperties( - toVariable - )}, relationship: ${projectAllProperties(relVariable)} }`, - ].join(", ")} }`; - } else { - return `{ ${[ - commonFieldsStr, - identifiersStr, - `relationshipName: ${typename}, fromLabels: ${eventMeta.fromLabels}, toLabels: ${eventMeta.toLabels}`, - `properties: { from: ${eventMeta.fromProperties}, to: ${ - eventMeta.toProperties - }, relationship: ${projectAllProperties(relVariable)} }`, - ].join(", ")} }`; - } -} diff --git a/packages/graphql/src/translate/subscriptions/create-event-meta.test.ts b/packages/graphql/src/translate/subscriptions/create-event-meta.test.ts deleted file mode 100644 index 5754a2e594..0000000000 --- a/packages/graphql/src/translate/subscriptions/create-event-meta.test.ts +++ /dev/null @@ -1,40 +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 { createEventMeta } from "./create-event-meta"; - -describe("createEventMeta", () => { - test("create", () => { - expect(createEventMeta({ event: "create", nodeVariable: "this0", typename: "MyNode" })).toBe( - `meta + { event: "create", id: id(this0), properties: { old: null, new: this0 { .* } }, timestamp: timestamp(), typename: "MyNode" } AS meta` - ); - }); - - test("update", () => { - expect(createEventMeta({ event: "update", nodeVariable: "this", typename: "MyNode" })).toBe( - `meta + { event: "update", id: id(this), properties: { old: oldProps, new: this { .* } }, timestamp: timestamp(), typename: "MyNode" } AS meta` - ); - }); - - test("delete", () => { - expect(createEventMeta({ event: "delete", nodeVariable: "this", typename: "MyNode" })).toBe( - `meta + { event: "delete", id: id(this), properties: { old: this { .* }, new: null }, timestamp: timestamp(), typename: "MyNode" } AS meta` - ); - }); -}); diff --git a/packages/graphql/src/translate/subscriptions/create-event-meta.ts b/packages/graphql/src/translate/subscriptions/create-event-meta.ts deleted file mode 100644 index 001b375bfa..0000000000 --- a/packages/graphql/src/translate/subscriptions/create-event-meta.ts +++ /dev/null @@ -1,66 +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 { META_CYPHER_VARIABLE, META_OLD_PROPS_CYPHER_VARIABLE } from "../../constants"; - -type SubscriptionsEventType = "create" | "update" | "delete"; - -type EventMetaParameters = { - event: SubscriptionsEventType; - nodeVariable: string; - typename: string; -}; - -export function createEventMeta(params: EventMetaParameters): string { - return `${META_CYPHER_VARIABLE} + ${createEventMetaObject(params)} AS ${META_CYPHER_VARIABLE}`; -} - -export function createEventMetaObject({ event, nodeVariable, typename }: EventMetaParameters): string { - const properties = createEventMetaProperties({ event, nodeVariable }); - return `{ event: "${event}", id: id(${nodeVariable}), ${properties}, timestamp: timestamp(), typename: "${typename}" }`; -} - -function createEventMetaProperties({ - event, - nodeVariable, -}: { - event: SubscriptionsEventType; - nodeVariable: string; -}): string { - let oldProps: string; - let newProps: string; - - switch (event) { - case "create": - oldProps = "null"; - newProps = `${nodeVariable} { .* }`; - break; - case "update": - oldProps = META_OLD_PROPS_CYPHER_VARIABLE; - newProps = `${nodeVariable} { .* }`; - break; - case "delete": - oldProps = `${nodeVariable} { .* }`; - newProps = "null"; - break; - // no default - } - - return `properties: { old: ${oldProps}, new: ${newProps} }`; -} diff --git a/packages/graphql/src/translate/subscriptions/filter-meta-variable.ts b/packages/graphql/src/translate/subscriptions/filter-meta-variable.ts deleted file mode 100644 index 1dea27bf1b..0000000000 --- a/packages/graphql/src/translate/subscriptions/filter-meta-variable.ts +++ /dev/null @@ -1,24 +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 { META_CYPHER_VARIABLE } from "../../constants"; - -export function filterMetaVariable(withVars: string[]): string[] { - return withVars.filter((w) => w !== META_CYPHER_VARIABLE); -} diff --git a/packages/graphql/src/translate/translate-aggregate.ts b/packages/graphql/src/translate/translate-aggregate.ts deleted file mode 100644 index 0a81bc7f64..0000000000 --- a/packages/graphql/src/translate/translate-aggregate.ts +++ /dev/null @@ -1,48 +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 Cypher from "@neo4j/cypher-builder"; -import Debug from "debug"; -import { DEBUG_TRANSLATE } from "../constants"; -import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; -import { buildClause } from "./utils/build-clause"; - -const debug = Debug(DEBUG_TRANSLATE); - -export function translateAggregate({ - context, - entityAdapter, -}: { - context: Neo4jGraphQLTranslationContext; - entityAdapter: EntityAdapter; -}): Cypher.CypherResult { - const { resolveTree } = context; - // TODO: Rename QueryAST to OperationsTree - const queryASTFactory = new QueryASTFactory(context.schemaModel); - - if (!entityAdapter) { - throw new Error("Entity not found"); - } - const queryAST = queryASTFactory.createQueryAST({ resolveTree, entityAdapter, context }); - debug(queryAST.print()); - const clause = queryAST.buildNew(context); - return buildClause(clause, { context }); -} diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index 4dfb4aa7d3..a5996ca17d 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -20,7 +20,6 @@ import type Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; import type { ResolveTree } from "graphql-parse-resolve-info"; -import type { Node } from "../classes"; import { DEBUG_TRANSLATE } from "../constants"; import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; @@ -30,6 +29,7 @@ import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; import { CallbackBucket } from "./queryAST/utils/callback-bucket"; import unwindCreate from "./unwind-create"; import { buildClause } from "./utils/build-clause"; +import { type ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; const debug = Debug(DEBUG_TRANSLATE); @@ -65,18 +65,14 @@ async function translateUsingQueryAST({ return buildClause(clause, { context }); } -export default async function translateCreate({ +export async function translateCreate({ context, - node, + entityAdapter, }: { context: Neo4jGraphQLTranslationContext; - node: Node; + entityAdapter: ConcreteEntityAdapter; }): Promise<{ cypher: string; params: Record }> { const { resolveTree } = context; - const entityAdapter = context.schemaModel.getConcreteEntityAdapter(node.name); - if (!entityAdapter) { - throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); - } const mutationInputs = resolveTree.args.input as any[]; const { isSupported, reason } = isUnwindCreateSupported(entityAdapter, asArray(mutationInputs), context); if (isSupported) { diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index 9a051c3d7c..0dcd525319 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -22,22 +22,22 @@ import Debug from "debug"; import { DEBUG_TRANSLATE } from "../constants"; import type { AuthenticationOperation } from "../schema-model/annotation/AuthenticationAnnotation"; import { getEntityAdapter } from "../schema-model/utils/get-entity-adapter"; -import type { CypherField } from "../types"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; import { applyAuthentication } from "./authorization/utils/apply-authentication"; import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext"; import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; import { buildClause } from "./utils/build-clause"; +import { type AttributeAdapter } from "../schema-model/attribute/model-adapters/AttributeAdapter"; const debug = Debug(DEBUG_TRANSLATE); export function translateTopLevelCypher({ context, - field, + attributeAdapter, type, }: { context: Neo4jGraphQLTranslationContext; - field: CypherField; + attributeAdapter: AttributeAdapter; type: "Query" | "Mutation"; }): Cypher.CypherResult { @@ -45,12 +45,11 @@ export function translateTopLevelCypher({ if (!operation) { throw new Error(`Failed to find operation ${type} in Schema Model.`); } - const operationField = operation.findAttribute(field.fieldName); + const operationField = operation.findAttribute(attributeAdapter.name); if (!operationField) { - throw new Error(`Failed to find field ${field.fieldName} on operation ${type}.`); + throw new Error(`Failed to find field ${attributeAdapter.name} on operation ${type}.`); } - const entity = context.schemaModel.entities.get(field.typeMeta.name); - + const entity = context.schemaModel.entities.get(attributeAdapter.getTypeName()); const annotation = operationField.annotations.authentication; if (annotation) { const targetOperations: AuthenticationOperation[] = diff --git a/packages/graphql/src/translate/translate-top-level-match.ts b/packages/graphql/src/translate/translate-top-level-match.ts deleted file mode 100644 index dda78c710f..0000000000 --- a/packages/graphql/src/translate/translate-top-level-match.ts +++ /dev/null @@ -1,158 +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 Cypher from "@neo4j/cypher-builder"; -import type { Node } from "../classes"; -import type { AuthorizationOperation } from "../schema-model/annotation/AuthorizationAnnotation"; -import type { GraphQLWhereArg, PredicateReturn } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { getEntityAdapterFromNode } from "../utils/get-entity-adapter-from-node"; -import { createAuthorizationBeforePredicate } from "./authorization/create-authorization-before-predicate"; -import { buildClause } from "./utils/build-clause"; -import { createWhereNodePredicate } from "./where/create-where-predicate"; - -export function translateTopLevelMatch({ - matchNode, - matchPattern, - node, - context, - operation, - where, - ignoreOperationAuthorization = false, -}: { - matchNode: Cypher.Node; - matchPattern: Cypher.Pattern; - context: Neo4jGraphQLTranslationContext; - node: Node; - operation: AuthorizationOperation; - where: GraphQLWhereArg | undefined; - ignoreOperationAuthorization: boolean; -}): Cypher.CypherResult { - const { matchClause, preComputedWhereFieldSubqueries, whereClause } = createMatchClause({ - matchNode, - matchPattern, - node, - context, - operation, - where, - ignoreOperationAuthorization, - }); - - return buildClause(Cypher.utils.concat(matchClause, preComputedWhereFieldSubqueries, whereClause), { context }); -} - -type CreateMatchClauseReturn = { - matchClause: Cypher.Match | Cypher.Yield; - preComputedWhereFieldSubqueries: Cypher.CompositeClause | undefined; - whereClause: Cypher.Match | Cypher.Yield | Cypher.With | undefined; -}; - -function createMatchClause({ - matchNode, - matchPattern, - node, - context, - operation, - ignoreOperationAuthorization, - where, -}: { - matchNode: Cypher.Node; - matchPattern: Cypher.Pattern; - context: Neo4jGraphQLTranslationContext; - node: Node; - operation: AuthorizationOperation; - where: GraphQLWhereArg | undefined; - ignoreOperationAuthorization: boolean; -}): CreateMatchClauseReturn { - const matchClause: Cypher.Match | Cypher.Yield = new Cypher.Match(matchPattern); - const whereOperators: Cypher.Predicate[] = []; - - let whereClause: Cypher.Match | Cypher.Yield | Cypher.With | undefined; - - let authorizationPredicateReturn: PredicateReturn | undefined; - if (operation === "UPDATE" && ignoreOperationAuthorization) { - whereClause = matchClause; - authorizationPredicateReturn = undefined; - } else { - authorizationPredicateReturn = createAuthorizationBeforePredicate({ - context, - nodes: [ - { - variable: matchNode, - node, - }, - ], - operations: [operation], - }); - if (authorizationPredicateReturn?.predicate) { - whereClause = new Cypher.With("*"); - } else { - whereClause = matchClause; - } - } - - let preComputedWhereFieldSubqueries: Cypher.CompositeClause | undefined; - if (where) { - const entity = getEntityAdapterFromNode(node, context); - - const { predicate: whereOp, preComputedSubqueries } = createWhereNodePredicate({ - targetElement: matchNode, - whereInput: where, - context, - entity, - }); - - preComputedWhereFieldSubqueries = preComputedSubqueries; - - if (preComputedWhereFieldSubqueries && !preComputedWhereFieldSubqueries.empty) { - whereClause = new Cypher.With("*"); - } - if (whereOp) whereClause.where(whereOp); - } - - if (whereOperators && whereOperators.length) { - const andChecks = Cypher.and(...whereOperators); - whereClause.where(andChecks); - } - - if (authorizationPredicateReturn) { - const { predicate, preComputedSubqueries } = authorizationPredicateReturn; - - if (predicate) { - whereClause.where(predicate); - } - - if (preComputedSubqueries && !preComputedSubqueries.empty) { - preComputedWhereFieldSubqueries = Cypher.utils.concat( - preComputedWhereFieldSubqueries, - preComputedSubqueries - ); - } - } - - if (matchClause === whereClause) { - whereClause = undefined; - } - - return { - matchClause, - preComputedWhereFieldSubqueries, - whereClause, - }; -} diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts deleted file mode 100644 index 1dec53b0a9..0000000000 --- a/packages/graphql/src/translate/translate-update.ts +++ /dev/null @@ -1,475 +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 Cypher from "@neo4j/cypher-builder"; -import Debug from "debug"; -import type { Node, Relationship } from "../classes"; -import { CallbackBucketDeprecated } from "../classes/CallbackBucketDeprecated"; -import { DEBUG_TRANSLATE } from "../constants"; -import type { GraphQLWhereArg, RelationField } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { compileCypher } from "../utils/compile-cypher"; -import { getRelationshipType } from "../utils/get-relationship-type"; -import createConnectAndParams from "./create-connect-and-params"; -import createCreateAndParams from "./create-create-and-params"; -import createDeleteAndParams from "./create-delete-and-params"; -import createDisconnectAndParams from "./create-disconnect-and-params"; -import { createSetRelationshipProperties } from "./create-set-relationship-properties"; -import createUpdateAndParams from "./create-update-and-params"; -import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext"; -import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; -import { CallbackBucket } from "./queryAST/utils/callback-bucket"; -import { translateTopLevelMatch } from "./translate-top-level-match"; -import { buildClause } from "./utils/build-clause"; -import { getAuthorizationStatements } from "./utils/get-authorization-statements"; - -const debug = Debug(DEBUG_TRANSLATE); - -export default async function translateUpdate({ - node, - context, -}: { - node: Node; - context: Neo4jGraphQLTranslationContext; -}): Promise<[string, any]> { - const { resolveTree } = context; - const updateInput = resolveTree.args.update; - const connectInput = resolveTree.args.connect; - const disconnectInput = resolveTree.args.disconnect; - const createInput = resolveTree.args.create; - const deleteInput = resolveTree.args.delete; - const varName = "this"; - const callbackBucket: CallbackBucketDeprecated = new CallbackBucketDeprecated(context); - const withVars = [varName]; - - let matchAndWhereStr = ""; - let updateStr = ""; - const connectStrs: string[] = []; - const disconnectStrs: string[] = []; - const createStrs: string[] = []; - let deleteStr = ""; - const matchNode = new Cypher.NamedNode(varName); - const where = resolveTree.args.where as GraphQLWhereArg | undefined; - const matchPattern = new Cypher.Pattern(matchNode, { labels: node.getLabels(context) }); - // bypass auth rules if there's no actual update of properties (connect and disconnect have their own auth rules) - const ignoreOperationAuthorization = updateInput - ? Object.keys(updateInput).every((x) => node.relationFields.some((field) => field.fieldName === x)) - : false; - const topLevelMatch = translateTopLevelMatch({ - matchNode, - matchPattern, - node, - context, - operation: "UPDATE", - where, - ignoreOperationAuthorization, - }); - matchAndWhereStr = topLevelMatch.cypher; - let cypherParams = topLevelMatch.params; - - const connectionStrs: string[] = []; - const interfaceStrs: string[] = []; - let updateArgs = {}; - - if (deleteInput) { - const deleteAndParams = createDeleteAndParams({ - context, - node, - deleteInput, - varName: `${varName}_delete`, - parentVar: varName, - withVars, - parameterPrefix: `${resolveTree.name}.args.delete`, - }); - [deleteStr] = deleteAndParams; - cypherParams = { - ...cypherParams, - ...deleteAndParams[1], - }; - updateArgs = { - ...updateArgs, - ...(deleteStr.includes(resolveTree.name) ? { delete: deleteInput } : {}), - }; - } - - if (disconnectInput) { - Object.entries(disconnectInput).forEach((entry) => { - const relationField = node.relationFields.find((x) => x.fieldName === entry[0]) as RelationField; - const refNodes: Node[] = []; - - if (relationField.union) { - Object.keys(entry[1]).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface?.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - if (relationField.interface) { - const disconnectAndParams = createDisconnectAndParams({ - context, - parentVar: varName, - refNodes, - relationField, - value: entry[1], - varName: `${varName}_disconnect_${entry[0]}`, - withVars, - parentNode: node, - parameterPrefix: `${resolveTree.name}.args.disconnect.${entry[0]}`, - labelOverride: "", - }); - disconnectStrs.push(disconnectAndParams[0]); - cypherParams = { ...cypherParams, ...disconnectAndParams[1] }; - } else { - refNodes.forEach((refNode) => { - const disconnectAndParams = createDisconnectAndParams({ - context, - parentVar: varName, - refNodes: [refNode], - relationField, - value: relationField.union ? entry[1][refNode.name] : entry[1], - varName: `${varName}_disconnect_${entry[0]}${relationField.union ? `_${refNode.name}` : ""}`, - withVars, - parentNode: node, - parameterPrefix: `${resolveTree.name}.args.disconnect.${entry[0]}${ - relationField.union ? `.${refNode.name}` : "" - }`, - labelOverride: relationField.union ? refNode.name : "", - }); - disconnectStrs.push(disconnectAndParams[0]); - cypherParams = { ...cypherParams, ...disconnectAndParams[1] }; - }); - } - }); - - updateArgs = { - ...updateArgs, - disconnect: disconnectInput, - }; - } - - if (updateInput) { - const updateAndParams = createUpdateAndParams({ - context, - callbackBucket, - node, - updateInput, - varName, - parentVar: varName, - withVars, - parameterPrefix: `${resolveTree.name}.args.update`, - ignoreOperationAuthorization, - }); - [updateStr] = updateAndParams; - cypherParams = { - ...cypherParams, - ...updateAndParams[1], - }; - updateArgs = { - ...updateArgs, - ...(updateStr.includes(resolveTree.name) ? { update: updateInput } : {}), - }; - } - - if (connectInput) { - Object.entries(connectInput).forEach((entry) => { - const relationField = node.relationFields.find((x) => entry[0] === x.fieldName) as RelationField; - const relationFieldType = getRelationshipType(relationField, context.features); - const refNodes: Node[] = []; - - if (relationField.union) { - Object.keys(entry[1]).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface?.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - if (relationField.interface) { - if (!relationField.typeMeta.array) { - const inStr = relationField.direction === "IN" ? "<-" : "-"; - const outStr = relationField.direction === "OUT" ? "->" : "-"; - - const validatePredicates: string[] = []; - refNodes.forEach((refNode) => { - const validateRelationshipExistence = `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${refNode.name}))`; - validatePredicates.push(validateRelationshipExistence); - }); - - if (validatePredicates.length) { - connectStrs.push("WITH *"); - connectStrs.push( - `WHERE apoc.util.validatePredicate(${validatePredicates.join( - " OR " - )},'Relationship field "%s.%s" cannot have more than one node linked',["${ - relationField.connectionPrefix - }","${relationField.fieldName}"])` - ); - } - } - - const connectAndParams = createConnectAndParams({ - context, - callbackBucket, - parentVar: varName, - refNodes, - relationField, - value: entry[1], - varName: `${varName}_connect_${entry[0]}`, - withVars, - parentNode: node, - labelOverride: "", - source: "UPDATE", - }); - connectStrs.push(connectAndParams[0]); - cypherParams = { ...cypherParams, ...connectAndParams[1] }; - } else { - refNodes.forEach((refNode) => { - const connectAndParams = createConnectAndParams({ - context, - callbackBucket, - parentVar: varName, - refNodes: [refNode], - relationField, - value: relationField.union ? entry[1][refNode.name] : entry[1], - varName: `${varName}_connect_${entry[0]}${relationField.union ? `_${refNode.name}` : ""}`, - withVars, - parentNode: node, - labelOverride: relationField.union ? refNode.name : "", - source: "UPDATE", - }); - connectStrs.push(connectAndParams[0]); - cypherParams = { ...cypherParams, ...connectAndParams[1] }; - }); - } - }); - } - - if (createInput) { - Object.entries(createInput).forEach((entry) => { - const relationField = node.relationFields.find((x) => entry[0] === x.fieldName) as RelationField; - const relationFieldType = getRelationshipType(relationField, context.features); - - const refNodes: Node[] = []; - - if (relationField.union) { - Object.keys(entry[1]).forEach((unionTypeName) => { - refNodes.push(context.nodes.find((x) => x.name === unionTypeName) as Node); - }); - } else if (relationField.interface) { - relationField.interface?.implementations?.forEach((implementationName) => { - refNodes.push(context.nodes.find((x) => x.name === implementationName) as Node); - }); - } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); - } - - const inStr = relationField.direction === "IN" ? "<-" : "-"; - const outStr = relationField.direction === "OUT" ? "->" : "-"; - - refNodes.forEach((refNode) => { - let v = relationField.union ? entry[1][refNode.name] : entry[1]; - - if (relationField.interface) { - if (relationField.typeMeta.array) { - v = entry[1] - .filter((c) => Object.keys(c.node).includes(refNode.name)) - .map((c) => ({ edge: c.edge, node: c.node[refNode.name] })); - - if (!v.length) { - return; - } - } else { - if (!entry[1].node[refNode.name]) { - return; - } - v = { edge: entry[1].edge, node: entry[1].node[refNode.name] }; - } - } - - const creates = relationField.typeMeta.array ? v : [v]; - creates.forEach((create, index) => { - const baseName = `${varName}_create_${entry[0]}${ - relationField.union || relationField.interface ? `_${refNode.name}` : "" - }${index}`; - const nodeName = `${baseName}_node${relationField.interface ? `_${refNode.name}` : ""}`; - const propertiesName = `${baseName}_relationship`; - const relationVarName = relationField.properties ? propertiesName : ""; - const relTypeStr = `[${relationVarName}:${relationFieldType}]`; - - if (!relationField.typeMeta.array) { - createStrs.push("WITH *"); - - const validatePredicateTemplate = (condition: string) => - `WHERE apoc.util.validatePredicate(${condition},'Relationship field "%s.%s" cannot have more than one node linked',["${relationField.connectionPrefix}","${relationField.fieldName}"])`; - - const singleCardinalityValidationTemplate = (nodeName) => - `EXISTS((${varName})${inStr}[:${relationFieldType}]${outStr}(:${nodeName}))`; - - if (relationField.union && relationField.union.nodes) { - const validateRelationshipExistence = relationField.union.nodes.map( - singleCardinalityValidationTemplate - ); - createStrs.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); - } else if (relationField.interface && relationField.interface.implementations) { - const validateRelationshipExistence = relationField.interface.implementations.map( - singleCardinalityValidationTemplate - ); - createStrs.push(validatePredicateTemplate(validateRelationshipExistence.join(" OR "))); - } else { - const validateRelationshipExistence = singleCardinalityValidationTemplate(refNode.name); - createStrs.push(validatePredicateTemplate(validateRelationshipExistence)); - } - } - - const { - create: nestedCreate, - params, - authorizationPredicates, - authorizationSubqueries, - } = createCreateAndParams({ - context, - callbackBucket, - node: refNode, - input: create.node, - varName: nodeName, - withVars: [...withVars, nodeName], - }); - createStrs.push(nestedCreate); - cypherParams = { ...cypherParams, ...params }; - createStrs.push(`MERGE (${varName})${inStr}${relTypeStr}${outStr}(${nodeName})`); - - if (relationField.properties) { - const relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; - - const setA = createSetRelationshipProperties({ - properties: create.edge ?? {}, - varName: propertiesName, - withVars, - relationship, - operation: "CREATE", - callbackBucket, - parameterPrefix: "", - parameterNotation: ".", - }); - if (setA) { - createStrs.push(setA[0]); - cypherParams = { ...cypherParams, ...setA[1] }; - } - } - - creates.push(...getAuthorizationStatements(authorizationPredicates, authorizationSubqueries)); - }); - }); - }); - } - const entityAdapter = context.schemaModel.getConcreteEntityAdapter(node.name); - if (!entityAdapter) { - throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); - } - - const queryAST = new QueryASTFactory(context.schemaModel).createMutationAST({ - resolveTree, - entityAdapter, - context, - callbackBucket: new CallbackBucket(context), - }); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: new Cypher.NamedNode(varName), - env: queryASTEnv, - neo4jGraphQLContext: context, - returnVariable: new Cypher.NamedVariable("data"), - shouldCollect: true, - shouldDistinct: true, - }); - debug(queryAST.print()); - const queryASTResult = queryAST.transpile(queryASTContext); - - const projectionStatements = queryASTResult.clauses.length - ? Cypher.utils.concat(...queryASTResult.clauses) - : new Cypher.Return(new Cypher.Literal("Query cannot conclude with CALL")); - - const updateQuery = new Cypher.Raw((env) => { - const cypher = [ - matchAndWhereStr, - deleteStr, - disconnectStrs.join("\n"), - updateStr, - connectStrs.join("\n"), - createStrs.join("\n"), - ...(deleteStr.length || - connectStrs.length || - disconnectStrs.length || - createStrs.length || - connectionStrs.length || - isFollowedByASubquery(projectionStatements) - ? [`WITH *`] - : []), // When FOREACH is the last line of update 'Neo4jError: WITH is required between FOREACH and CALL' - - ...connectionStrs, - ...interfaceStrs, - compileCypher(projectionStatements, env), - ] - .filter(Boolean) - .join("\n"); - - return [ - cypher, - { - ...cypherParams, - ...(Object.keys(updateArgs).length ? { [resolveTree.name]: { args: updateArgs } } : {}), - }, - ]; - }); - - const cypherResult = buildClause(updateQuery, { context, prefix: "update_" }); - const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({ - cypher: cypherResult.cypher, - }); - const result: [string, Record] = [cypher, { ...cypherResult.params, resolvedCallbacks }]; - return result; -} - -/** - * Temporary helper to keep consistency with the old code where if a subquery was present, it would be followed by a WITH *. - * The recursion is needed because the subquery can be wrapped inside a Cypher.Composite. - **/ -function isFollowedByASubquery(clause): boolean { - if (clause.children?.length) { - if (clause.children[0] instanceof Cypher.Call) { - return true; - } - if (clause.children[0]?.children?.length) { - return isFollowedByASubquery(clause.children[0]); - } - } - return false; -} diff --git a/packages/graphql/src/translate/utils/assert-non-ambiguous-update.ts b/packages/graphql/src/translate/utils/assert-non-ambiguous-update.ts deleted file mode 100644 index c80f14adce..0000000000 --- a/packages/graphql/src/translate/utils/assert-non-ambiguous-update.ts +++ /dev/null @@ -1,33 +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 { Node, Relationship } from "../../classes"; -import { Neo4jGraphQLError } from "../../classes"; -import { findConflictingProperties } from "../../utils/find-conflicting-properties"; - -export function assertNonAmbiguousUpdate(graphElement: Relationship | Node, input: Record): void { - const conflictingProperties = findConflictingProperties({ graphElement, input }); - if (conflictingProperties.length > 0) { - throw new Neo4jGraphQLError( - `Conflicting modification of ${conflictingProperties.map((n) => `[[${n}]]`).join(", ")} on type ${ - graphElement.name - }` - ); - } -} diff --git a/packages/graphql/src/translate/utils/callback-utils.ts b/packages/graphql/src/translate/utils/callback-utils.ts deleted file mode 100644 index 9fdb34d004..0000000000 --- a/packages/graphql/src/translate/utils/callback-utils.ts +++ /dev/null @@ -1,77 +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 Cypher from "@neo4j/cypher-builder"; -import type { CallbackBucketDeprecated } from "../../classes/CallbackBucketDeprecated"; -import type { PrimitiveField } from "../../types"; -import { compileCypher } from "../../utils/compile-cypher"; - -export const addCallbackAndSetParam = ( - field: PrimitiveField, - varName: string, - parent: any, - callbackBucket: CallbackBucketDeprecated, - strs: string[], - operation: "CREATE" | "UPDATE" -): void => { - if (!field.callback || !field.callback.operations.includes(operation)) { - return; - } - - const paramName = `${varName}_${field.fieldName}_${field.callback?.callbackName}`; - - callbackBucket.addCallback({ - functionName: field.callback?.callbackName, - paramName, - parent, - type: field.typeMeta, - }); - - strs.push(`SET ${varName}.${field.dbPropertyName} = $resolvedCallbacks.${paramName}`); -}; - -export const addCallbackAndSetParamCypher = ( - field: PrimitiveField, - variable: Cypher.Variable, - parent: any, - callbackBucket: CallbackBucketDeprecated, - operation: "CREATE" | "UPDATE", - node: Cypher.Node -): [Cypher.Property, Cypher.Raw] | [] => { - if (!field.callback || !field.callback.operations.includes(operation)) { - return []; - } - - const propRef = node.property(field.dbPropertyName as string); - const rawCypherStatement = new Cypher.Raw((env) => { - const variableCypher = compileCypher(variable, env); - const paramName = `${variableCypher}_${field.fieldName}_${field.callback?.callbackName}`; - - callbackBucket.addCallback({ - functionName: field.callback?.callbackName as string, - paramName, - parent, - type: field.typeMeta, - }); - - return [`$resolvedCallbacks.${paramName}`, {}]; - }); - - return [propRef, rawCypherStatement]; -}; diff --git a/packages/graphql/src/translate/utils/get-authorization-statements.ts b/packages/graphql/src/translate/utils/get-authorization-statements.ts deleted file mode 100644 index 672edb8ee1..0000000000 --- a/packages/graphql/src/translate/utils/get-authorization-statements.ts +++ /dev/null @@ -1,33 +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. - */ - -export function getAuthorizationStatements(predicates: string[], subqueries: string[]) { - const statements: string[] = []; - - if (predicates.length) { - statements.push("WITH *"); - if (subqueries.length) { - statements.push(...subqueries); - statements.push("WITH *"); - } - statements.push(`WHERE ${predicates.join(" AND ")}`); - } - - return statements; -} diff --git a/packages/graphql/src/translate/utils/get-mutation-field-statements.ts b/packages/graphql/src/translate/utils/get-mutation-field-statements.ts deleted file mode 100644 index 12814ae6e7..0000000000 --- a/packages/graphql/src/translate/utils/get-mutation-field-statements.ts +++ /dev/null @@ -1,199 +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 { Neo4jGraphQLError, type Node, type Relationship } from "../../classes"; -import { SPATIAL_TYPES } from "../../constants"; -import mapToDbProperty from "../../utils/map-to-db-property"; -import { buildMathStatements, matchMathField, mathDescriptorBuilder } from "./math"; -import { parseMutableField } from "./parse-mutable-field"; - -/** - * Transforms a key-value pair such as: - * { name_SET: "John" } - * Into statements such as: - * SET node.name = $param - **/ -export function getMutationFieldStatements({ - nodeOrRel, - param, - key, - varName, - value, - withVars, - isUpdateOperation = false, -}: { - nodeOrRel: Node | Relationship; - param: string; - key: string; - varName: string; - value: any; - withVars: string[]; - isUpdateOperation?: boolean; -}): string { - const strs: string[] = []; - const { settableField, operator } = parseMutableField(nodeOrRel, key); - if (!operator && isUpdateOperation) { - const result = getMutationFieldStatementsForGenericOperator({ - nodeOrRel, - param, - key, - varName, - operations: value, - withVars, - }); - return result; - } - - if (settableField) { - const dbFieldName = mapToDbProperty(nodeOrRel, settableField.fieldName); - if (settableField.typeMeta.required && value === null) { - throw new Error(`Cannot set non-nullable field ${nodeOrRel.name}.${settableField.fieldName} to null`); - } - - switch (operator ?? "SET") { - case "SET": { - const isSpatial = SPATIAL_TYPES.includes(settableField.typeMeta.name); - const isZonedTemporal = ["DateTime", "Time"].includes(settableField.typeMeta.name); - if (isSpatial) { - if (settableField.typeMeta.array) { - strs.push(`SET ${varName}.${dbFieldName} = [p in $${param} | point(p)]`); - } else { - strs.push(`SET ${varName}.${dbFieldName} = point($${param})`); - } - } else if (isZonedTemporal) { - if (settableField.typeMeta.array) { - strs.push( - `SET ${varName}.${dbFieldName} = [t in $${param} | ${settableField.typeMeta.name.toLowerCase()}(t)]` - ); - } else { - strs.push( - `SET ${varName}.${dbFieldName} = ${settableField.typeMeta.name.toLowerCase()}($${param})` - ); - } - } else { - strs.push(`SET ${varName}.${dbFieldName} = $${param}`); - } - - break; - } - case "INCREMENT": - case "DECREMENT": - case "ADD": - case "SUBTRACT": - case "DIVIDE": - case "MULTIPLY": { - const mathMatch = matchMathField(key); - const mathDescriptor = mathDescriptorBuilder(value as number, nodeOrRel, mathMatch); - const mathStatements = buildMathStatements(mathDescriptor, varName, withVars, param); - strs.push(...mathStatements); - break; - } - case "PUSH": { - const pointArrayField = nodeOrRel.pointFields.find((x) => x.fieldName === settableField.fieldName); - const zonedTemporalArrayField = nodeOrRel.temporalFields.find( - (x) => - x.fieldName === settableField.fieldName && - ["DateTime", "Time"].includes(settableField.typeMeta.name) - ); - - if (pointArrayField) { - strs.push( - `SET ${varName}.${dbFieldName} = ${varName}.${dbFieldName} + [p in $${param} | point(p)]` - ); - } else if (zonedTemporalArrayField) { - strs.push( - `SET ${varName}.${dbFieldName} = ${varName}.${dbFieldName} + [t in $${param} | ${settableField.typeMeta.name.toLowerCase()}(t)]` - ); - } else { - strs.push(`SET ${varName}.${dbFieldName} = ${varName}.${dbFieldName} + $${param}`); - } - - break; - } - case "POP": { - strs.push(`SET ${varName}.${dbFieldName} = ${varName}.${dbFieldName}[0..-$${param}]`); - break; - } - } - } - return strs.join("\n"); -} - -// Converts generic operator into cypher statements using the deprecated syntax as intermediate step -function getMutationFieldStatementsForGenericOperator({ - nodeOrRel, - param, - key, - varName, - operations, - withVars, -}: { - nodeOrRel: Node | Relationship; - param: string; - key: string; - varName: string; - operations: any; - withVars: string[]; -}): string { - if (Object.entries(operations).length > 1) { - throw new Neo4jGraphQLError( - `Conflicting modification of field ${key}: ${Object.keys(operations) - .map((n) => `[[${n}]]`) - .join(", ")} on type ${nodeOrRel.name}` - ); - } - - return Object.entries(operations) - .map(([operator, value]) => { - return getMutationFieldStatements({ - nodeOrRel, - param: `${param}.${operator}`, - key: `${key}_${newOperatorToDeprecated(operator)}`, - varName, - withVars, - value, - }); - }) - .join("\n"); -} - -function newOperatorToDeprecated(op: string): string { - switch (op) { - case "set": - return "SET"; - case "increment": - return "INCREMENT"; - case "decrement": - return "DECREMENT"; - case "add": - return "ADD"; - case "subtract": - return "SUBTRACT"; - case "divide": - return "DIVIDE"; - case "multiply": - return "MULTIPLY"; - case "push": - return "PUSH"; - case "pop": - return "POP"; - default: - throw new Error(`Unknown generic mutation operator ${op}`); - } -} diff --git a/packages/graphql/src/translate/utils/get-or-create-cypher-variable.ts b/packages/graphql/src/translate/utils/get-or-create-cypher-variable.ts deleted file mode 100644 index f517b5648a..0000000000 --- a/packages/graphql/src/translate/utils/get-or-create-cypher-variable.ts +++ /dev/null @@ -1,30 +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 Cypher from "@neo4j/cypher-builder"; - -export function getOrCreateCypherNode(nameOrNode: Cypher.Node | string): Cypher.Node { - if (typeof nameOrNode === "string") return new Cypher.NamedNode(nameOrNode); - return nameOrNode; -} - -export function getOrCreateCypherVariable(nameOrVariable: Cypher.Variable | string): Cypher.Variable { - if (typeof nameOrVariable === "string") return new Cypher.NamedVariable(nameOrVariable); - return nameOrVariable; -} diff --git a/packages/graphql/src/translate/utils/get-relationship-direction.ts b/packages/graphql/src/translate/utils/get-relationship-direction.ts deleted file mode 100644 index d926d62ee1..0000000000 --- a/packages/graphql/src/translate/utils/get-relationship-direction.ts +++ /dev/null @@ -1,36 +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 { RelationshipQueryDirectionOption } from "../../constants"; -import type { RelationField } from "../../types"; - -export function getRelationshipDirection(relationshipField: RelationField): { inStr: string; outStr: string } { - const inStr = - relationshipField.direction === "IN" && - relationshipField.queryDirection !== RelationshipQueryDirectionOption.UNDIRECTED - ? "<-" - : "-"; - const outStr = - relationshipField.direction === "OUT" && - relationshipField.queryDirection !== RelationshipQueryDirectionOption.UNDIRECTED - ? "->" - : "-"; - - return { inStr, outStr }; -} diff --git a/packages/graphql/src/translate/utils/indent-block.ts b/packages/graphql/src/translate/utils/indent-block.ts deleted file mode 100644 index 3a2fa93656..0000000000 --- a/packages/graphql/src/translate/utils/indent-block.ts +++ /dev/null @@ -1,32 +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. - */ - -export function indentBlock(query: string): string; -export function indentBlock(query: string[]): string[]; -export function indentBlock(query: string | string[]): string | string[] { - if (typeof query === "string") { - const statements = query.split("\n"); - return indentStatements(statements).join("\n"); - } - return indentStatements(query); -} - -function indentStatements(statements: string[]): string[] { - return statements.map((s) => `\t${s}`); -} diff --git a/packages/graphql/src/translate/utils/math.ts b/packages/graphql/src/translate/utils/math.ts deleted file mode 100644 index 2664614730..0000000000 --- a/packages/graphql/src/translate/utils/math.ts +++ /dev/null @@ -1,146 +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 { GraphElement } from "../../classes"; -import mapToDbProperty from "../../utils/map-to-db-property"; - -/** Maps Neo4jGraphQL Math operator to Cypher symbol */ -const CypherOperatorMap = new Map([ - ["_ADD", "+"], - ["_SUBTRACT", "-"], - ["_MULTIPLY", "*"], - ["_DIVIDE", "/"], - ["_INCREMENT", "+"], - ["_DECREMENT", "-"], -]); - -function mathOperatorToSymbol(mathOperator: string): string { - if (CypherOperatorMap.has(mathOperator)) { - return CypherOperatorMap.get(mathOperator) as string; - } - throw new Error(`${mathOperator} is not a valid math operator`); -} - -const MATH_FIELD_REGEX = - /(?\w*)(?_INCREMENT|_DECREMENT|_ADD|_SUBTRACT|_DIVIDE|_MULTIPLY)\b/; - -type MatchRegexMatchGroups = { - propertyName: string; - operatorName: "_INCREMENT" | "_DECREMENT" | "_ADD" | "_SUBTRACT" | "_DIVIDE" | "_MULTIPLY"; -}; - -interface MathDescriptor { - dbName: string; - graphQLType: string; - fieldName: string; - operationName: string; - operationSymbol: string; - value: number; -} - -interface MathMatch { - hasMatched: boolean; - operatorName: string; - propertyName: string; -} -// Returns True in case of a valid match and the potential match. -export function matchMathField(graphQLFieldName: string): MathMatch { - const mathFieldMatch = graphQLFieldName.match(MATH_FIELD_REGEX); - if (mathFieldMatch && mathFieldMatch.groups) { - const { operatorName, propertyName } = mathFieldMatch.groups as MatchRegexMatchGroups; - const hasMatched = Boolean(mathFieldMatch && mathFieldMatch.length > 2 && operatorName && propertyName); - return { - hasMatched, - operatorName, - propertyName, - }; - } - return { - hasMatched: false, - operatorName: "", - propertyName: "", - }; -} - -export function mathDescriptorBuilder(value: number, entity: GraphElement, fieldMatch: MathMatch): MathDescriptor { - const fieldName = fieldMatch.propertyName; - const field = entity.primitiveFields.find((x) => x.fieldName === fieldName); - if (!field) { - throw new Error(`${fieldName} is not settable`); - } - return { - dbName: mapToDbProperty(entity, fieldName), - graphQLType: field.typeMeta.name, - fieldName, - operationName: fieldMatch.operatorName, - operationSymbol: mathOperatorToSymbol(fieldMatch.operatorName), - value, - }; -} - -export function buildMathStatements( - mathDescriptor: MathDescriptor, - scope: string, - withVars: string[], - param: string -): Array { - if (mathDescriptor.operationSymbol === "/" && mathDescriptor.value === 0) { - throw new Error("Division by zero is not supported"); - } - - const validatePredicates: string[] = []; - - // Raise for operations with NAN - validatePredicates.push( - `apoc.util.validatePredicate(${scope}.${mathDescriptor.dbName} IS NULL, 'Cannot %s %s to Nan', ["${mathDescriptor.operationName}", $${param}])` - ); - - const bitSize = mathDescriptor.graphQLType === "Int" ? 32 : 64; - // Avoid overflows, for 64 bit overflows, a long overflow is raised anyway by Neo4j - validatePredicates.push( - `apoc.util.validatePredicate(${scope}.${mathDescriptor.dbName} IS NOT NULL AND ${scope}.${ - mathDescriptor.dbName - } ${mathDescriptor.operationSymbol} $${param} > 2^${ - bitSize - 1 - }-1, 'Overflow: Value returned from operator %s is larger than %s bit', ["${ - mathDescriptor.operationName - }", "${bitSize}"])` - ); - - // Avoid type coercion where dividing an integer would result in a float value - if (mathDescriptor.operationSymbol === "/" && mathDescriptor.graphQLType.includes("Int")) { - validatePredicates.push( - `apoc.util.validatePredicate(${scope}.${mathDescriptor.dbName} IS NOT NULL AND (${scope}.${mathDescriptor.dbName} ${mathDescriptor.operationSymbol} $${param}) % 1 <> 0, 'Type Mismatch: Value returned from operator %s does not match: %s', ["${mathDescriptor.operationName}", "${mathDescriptor.graphQLType}"])` - ); - } - - const statements: string[] = []; - const mathScope = Array.from(new Set([scope, ...withVars])); - statements.push(`WITH ${mathScope.join(", ")}`); - statements.push(`CALL(${scope}) {`); - statements.push(`WITH ${scope}`); - // Validations - statements.push(`WHERE ${validatePredicates.join(" AND ")}`); - statements.push( - `SET ${scope}.${mathDescriptor.dbName} = ${scope}.${mathDescriptor.dbName} ${mathDescriptor.operationSymbol} $${param}` - ); - statements.push(`RETURN ${scope} as ${scope}_${mathDescriptor.dbName}_${mathDescriptor.operationName}`); - statements.push(`}`); - return statements; -} diff --git a/packages/graphql/src/translate/utils/parse-mutable-field.ts b/packages/graphql/src/translate/utils/parse-mutable-field.ts deleted file mode 100644 index 0501ed8134..0000000000 --- a/packages/graphql/src/translate/utils/parse-mutable-field.ts +++ /dev/null @@ -1,37 +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 { Node, Relationship } from "../../classes"; -import type { MutableField } from "../../classes/Node"; -import type { MutationOperator } from "../queryAST/factory/parsers/parse-mutation-field"; -import { parseMutationField } from "../queryAST/factory/parsers/parse-mutation-field"; - -export function parseMutableField( - nodeOrRel: Node | Relationship, - key: string -): { settableField: MutableField; operator: MutationOperator | undefined } { - const { fieldName, operator } = parseMutationField(key); - const field = nodeOrRel.mutableFields.find((x) => x.fieldName === fieldName); - - if (field) { - return { settableField: field, operator: operator }; - } - - throw new Error(`Transpile error: field ${key} not found`); -} diff --git a/packages/graphql/src/translate/where/create-connection-where-and-params.ts b/packages/graphql/src/translate/where/create-connection-where-and-params.ts deleted file mode 100644 index 26a5f8bb69..0000000000 --- a/packages/graphql/src/translate/where/create-connection-where-and-params.ts +++ /dev/null @@ -1,72 +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 Cypher from "@neo4j/cypher-builder"; -import type { Node, Relationship } from "../../classes"; -import type { ConnectionWhereArg } from "../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; -import { compileCypher } from "../../utils/compile-cypher"; -import { buildClause } from "../utils/build-clause"; -import { createConnectionWherePropertyOperation } from "./property-operations/create-connection-operation"; - -export default function createConnectionWhereAndParams({ - whereInput, - context, - node, - nodeVariable, - relationship, - relationshipVariable, - parameterPrefix, -}: { - whereInput: ConnectionWhereArg; - context: Neo4jGraphQLTranslationContext; - node: Node; - nodeVariable: string; - relationship: Relationship; - relationshipVariable: string; - parameterPrefix: string; -}): { cypher: string; subquery: string; params: Record } { - const nodeRef = new Cypher.NamedNode(nodeVariable); - const edgeRef = new Cypher.NamedVariable(relationshipVariable); - - const { predicate: andOp, preComputedSubqueries } = createConnectionWherePropertyOperation({ - context, - whereInput, - edgeRef, - targetNode: nodeRef, - node, - edge: relationship, - }); - - let subquery = ""; - const whereCypher = new Cypher.Raw((env) => { - const cypher = andOp ? env.compile(andOp) : ""; - if (preComputedSubqueries) { - subquery = compileCypher(preComputedSubqueries, env); - } - return [cypher, {}]; - }); - - // NOTE: the following prefix is just to avoid collision until this is refactored into a single cypher ast - const result = buildClause(whereCypher, { - context, - prefix: `${parameterPrefix.replace(/\./g, "_").replace(/\[|\]/g, "")}_${nodeVariable}`, - }); - return { cypher: result.cypher, subquery, params: result.params }; -} diff --git a/packages/graphql/src/translate/where/create-where-predicate.ts b/packages/graphql/src/translate/where/create-where-predicate.ts deleted file mode 100644 index 6b5879c43e..0000000000 --- a/packages/graphql/src/translate/where/create-where-predicate.ts +++ /dev/null @@ -1,138 +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 Cypher from "@neo4j/cypher-builder"; -import type { EntityAdapter } from "../../schema-model/entity/EntityAdapter"; -import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { GraphQLWhereArg } from "../../types"; -import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; -import { QueryASTContext, QueryASTEnv } from "../queryAST/ast/QueryASTContext"; -import type { Filter } from "../queryAST/ast/filters/Filter"; -import { QueryASTFactory } from "../queryAST/factory/QueryASTFactory"; -import { isInterfaceEntity } from "../queryAST/utils/is-interface-entity"; -import { wrapSubqueriesInCypherCalls } from "../queryAST/utils/wrap-subquery-in-calls"; - -function createWherePredicate({ - factory, - queryASTContext, - entityOrRel, - whereInput, - targetElement, - targetEntity, -}: { - factory: QueryASTFactory; - queryASTContext: QueryASTContext; - entityOrRel: EntityAdapter | RelationshipAdapter; - whereInput: GraphQLWhereArg; - targetElement: Cypher.Node | Cypher.Relationship; - targetEntity?: ConcreteEntityAdapter; // It's required for interface entities to be passed in -}): { - predicate: Cypher.Predicate | undefined; - preComputedSubqueries?: Cypher.CompositeClause | undefined; -} { - const filters: Filter[] = []; - if (entityOrRel instanceof RelationshipAdapter) { - filters.push(...factory.filterFactory.createEdgeFilters(entityOrRel, whereInput)); - } else if (isInterfaceEntity(entityOrRel)) { - filters.push( - ...factory.filterFactory.createInterfaceNodeFilters({ - entity: entityOrRel, - targetEntity, - whereFields: whereInput, - }) - ); - } else { - filters.push(...factory.filterFactory.createNodeFilters(entityOrRel, whereInput)); - } - - const subqueries = wrapSubqueriesInCypherCalls(queryASTContext, filters, [targetElement]); - const predicates = filters.map((f) => f.getPredicate(queryASTContext)); - const extraSelections = filters.flatMap((f) => f.getSelection(queryASTContext)); - - const preComputedSubqueries = [...extraSelections, ...subqueries]; - - return { - predicate: Cypher.and(...predicates), - preComputedSubqueries: Cypher.utils.concat(...preComputedSubqueries), - }; -} - -export function createWhereNodePredicate({ - targetElement, - whereInput, - context, - entity, - targetEntity, -}: { - targetElement: Cypher.Node; - whereInput: GraphQLWhereArg; - context: Neo4jGraphQLTranslationContext; - entity: EntityAdapter; - targetEntity?: ConcreteEntityAdapter; -}): { - predicate: Cypher.Predicate | undefined; - preComputedSubqueries?: Cypher.CompositeClause | undefined; -} { - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: targetElement, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - return createWherePredicate({ - factory, - queryASTContext, - entityOrRel: entity, - whereInput, - targetElement, - targetEntity, - }); -} - -export function createWhereEdgePredicate({ - targetElement, - relationshipVariable, - whereInput, - context, - relationship, -}: { - targetElement: Cypher.Node; - relationshipVariable: Cypher.Relationship; - whereInput: GraphQLWhereArg; - context: Neo4jGraphQLTranslationContext; - relationship: RelationshipAdapter; -}): { - predicate: Cypher.Predicate | undefined; - preComputedSubqueries?: Cypher.CompositeClause | undefined; -} { - const factory = new QueryASTFactory(context.schemaModel); - const queryASTEnv = new QueryASTEnv(); - - const queryASTContext = new QueryASTContext({ - target: targetElement, - relationship: relationshipVariable, - env: queryASTEnv, - neo4jGraphQLContext: context, - }); - - return createWherePredicate({ factory, queryASTContext, entityOrRel: relationship, whereInput, targetElement }); -} diff --git a/packages/graphql/src/translate/where/property-operations/create-connection-operation.ts b/packages/graphql/src/translate/where/property-operations/create-connection-operation.ts deleted file mode 100644 index 7d193f7974..0000000000 --- a/packages/graphql/src/translate/where/property-operations/create-connection-operation.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 Cypher from "@neo4j/cypher-builder"; -import type { Node, Relationship } from "../../../classes"; -import type { ConnectionWhereArg, PredicateReturn } from "../../../types"; -// Recursive function - -import type { RelationshipAdapter } from "../../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import { getEntityAdapterFromNode } from "../../../utils/get-entity-adapter-from-node"; -import { asArray, filterTruthy } from "../../../utils/utils"; -import { getLogicalPredicate, isLogicalOperator } from "../../utils/logical-operators"; -import { createWhereEdgePredicate, createWhereNodePredicate } from "../create-where-predicate"; - -export function createConnectionWherePropertyOperation({ - context, - whereInput, - edgeRef, - targetNode, - node, - edge, - useExistExpr = true, - checkParameterExistence, -}: { - whereInput: ConnectionWhereArg; - context: Neo4jGraphQLTranslationContext; - node: Node; - edge: Relationship; - edgeRef: Cypher.Variable; - targetNode: Cypher.Node; - useExistExpr?: boolean; - checkParameterExistence?: boolean; -}): PredicateReturn { - const preComputedSubqueriesResult: (Cypher.CompositeClause | undefined)[] = []; - const params: (Cypher.Predicate | undefined)[] = []; - Object.entries(whereInput).forEach(([key, value]) => { - if (isLogicalOperator(key)) { - const subOperations: (Cypher.Predicate | undefined)[] = []; - asArray(value).forEach((input) => { - const { predicate, preComputedSubqueries } = createConnectionWherePropertyOperation({ - context, - whereInput: input, - edgeRef, - targetNode, - node, - edge, - useExistExpr, - checkParameterExistence, - }); - subOperations.push(predicate); - if (preComputedSubqueries && !preComputedSubqueries.empty) - preComputedSubqueriesResult.push(preComputedSubqueries); - }); - - const logicalPredicate = getLogicalPredicate(key, filterTruthy(subOperations)); - params.push(logicalPredicate); - return; - } - - if (key.startsWith("edge")) { - const entity = context.schemaModel.getConcreteEntityAdapter(edge.source); - if (!entity) { - throw new Error(`Transpilation error: entity not found: ${edge.source}`); - } - const relationshipAdapter = [...entity.relationships.values()].find( - (r: RelationshipAdapter): r is RelationshipAdapter => - r.operations.relationshipFieldTypename === edge.name - ); - if (!relationshipAdapter) { - throw new Error(`No relationship found for ${edge.name}`); - } - - const { predicate: result, preComputedSubqueries } = createWhereEdgePredicate({ - targetElement: targetNode, - relationshipVariable: edgeRef as Cypher.Relationship, - relationship: relationshipAdapter, - context, - whereInput: value, - }); - - params.push(result); - if (preComputedSubqueries && !preComputedSubqueries.empty) { - preComputedSubqueriesResult.push(preComputedSubqueries); - } - return; - } - - if (key.startsWith("node") || key.startsWith(node.name)) { - // TODO: improve nodeOn properties generation - const nestedProperties = { ...value }; - - const entity = getEntityAdapterFromNode(node, context); - const { predicate: result, preComputedSubqueries } = createWhereNodePredicate({ - entity, - context, - whereInput: nestedProperties, - targetElement: targetNode, - }); - - // NOTE: _NOT is handled by the size()=0 - params.push(result); - if (preComputedSubqueries && !preComputedSubqueries.empty) - preComputedSubqueriesResult.push(preComputedSubqueries); - return; - } - }); - return { - predicate: Cypher.and(...filterTruthy(params)), - preComputedSubqueries: preComputedSubqueriesResult.length - ? Cypher.utils.concat(...preComputedSubqueriesResult) - : undefined, - }; -} diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index f46ce84b73..596f6a7e5b 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -19,23 +19,18 @@ import type Cypher from "@neo4j/cypher-builder"; import type { EventEmitter } from "events"; -import type { DirectiveNode, InputValueDefinitionNode, TypeNode } from "graphql"; +import type { TypeNode } from "graphql"; import type { Directive } from "graphql-compose"; -import type { ResolveTree } from "graphql-parse-resolve-info"; import type { JWTVerifyOptions, RemoteJWKSetOptions } from "jose"; import type { Integer } from "neo4j-driver"; import type { Neo4jGraphQLSubscriptionsCDCEngine } from "../classes/subscription/Neo4jGraphQLSubscriptionsCDCEngine"; -import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../constants"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; import type { DefaultAnnotationValue } from "../schema-model/annotation/DefaultAnnotation"; import type { FulltextField } from "../schema-model/annotation/FulltextAnnotation"; import type { VectorField } from "../schema-model/annotation/VectorAnnotation"; -import type { RelationshipDirection } from "../schema-model/relationship/Relationship"; import type { JwtPayload } from "./jwt-payload"; import type { Neo4jGraphQLContext } from "./neo4j-graphql-context"; -export { Node } from "../classes"; - export type AuthorizationContext = { jwt?: JwtPayload; jwtParam: Cypher.Param; @@ -59,10 +54,6 @@ export type VectorContext = { vectorSettings: Neo4jVectorSettings; }; -export type FullText = { - indexes: FulltextContext[]; -}; - /** * Metadata about a field.type on either * FieldDefinitionNode or InputValueDefinitionNode. @@ -89,122 +80,6 @@ export interface TypeMeta { originalType?: TypeNode; } -export type Unique = { - constraintName: string; -}; - -export interface Callback { - operations: CallbackOperations[]; - callbackName: string; -} - -export type SelectableOptions = { - onRead: boolean; - onAggregate: boolean; -}; - -export type SettableOptions = { - onCreate: boolean; - onUpdate: boolean; -}; - -export type FilterableOptions = { - byValue: boolean; - byAggregate: boolean; -}; - -/** - * Representation a ObjectTypeDefinitionNode field. - */ -export interface BaseField { - fieldName: string; - typeMeta: TypeMeta; - otherDirectives: DirectiveNode[]; - arguments: InputValueDefinitionNode[]; - private?: boolean; - description?: string; - dbPropertyName?: string; - dbPropertyNameUnescaped?: string; - unique?: Unique; - selectableOptions: SelectableOptions; - settableOptions: SettableOptions; - filterableOptions: FilterableOptions; -} - -/** - * Representation of the `@relationship` directive and its meta. - */ -export interface RelationField extends BaseField { - direction: RelationshipDirection; - typeUnescaped: string; - type: string; - connectionPrefix?: string; - inherited: boolean; - properties?: string; - union?: UnionField; - interface?: InterfaceField; - queryDirection: RelationshipQueryDirectionOption; - nestedOperations: RelationshipNestedOperationsOption[]; - aggregate: boolean; -} - -export interface ConnectionField extends BaseField { - relationship: RelationField; - relationshipTypeName: string; -} - -/** - * Representation of the `@cypher` directive and its meta. - */ -export interface CypherField extends BaseField { - statement: string; - columnName: string; - isEnum: boolean; - isScalar: boolean; -} - -/** - * Representation of any field thats not - * a cypher directive or relationship directive - * String, Int, Float, ID, Boolean... (custom scalars). - */ -export interface PrimitiveField extends BaseField { - autogenerate?: boolean; - defaultValue?: any; - coalesceValue?: any; - callback?: Callback; - isGlobalIdField?: boolean; -} - -export type CustomScalarField = BaseField; - -export interface CustomEnumField extends BaseField { - // TODO Must be "Enum" - really needs refactoring into classes - kind: string; - defaultValue?: string | string[]; - coalesceValue?: string | string[]; -} - -export interface UnionField extends BaseField { - nodes?: string[]; -} - -export interface CustomResolverField extends BaseField { - requiredFields: Record; -} - -export interface InterfaceField extends BaseField { - implementations?: string[]; -} - -export type ObjectField = BaseField; - -export interface TemporalField extends PrimitiveField { - timestamps?: TimeStampOperations[]; -} - -export type PointField = BaseField; - export type SortDirection = "ASC" | "DESC"; export interface GraphQLSortArg { @@ -256,10 +131,6 @@ export interface ConnectionWhereArg { NOT?: ConnectionWhereArg; } -export type TimeStampOperations = "CREATE" | "UPDATE"; - -export type CallbackOperations = "CREATE" | "UPDATE"; - /* Object keys and enum values map to values at https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/#cypher-query-options */ @@ -281,43 +152,6 @@ export interface CypherQueryOptions { /** Input field for graphql-compose */ export type InputField = { type: string; defaultValue?: DefaultAnnotationValue; directives?: Directive[] } | string; -/** Raw event metadata returned from queries */ -export type NodeSubscriptionMeta = { - event: "create" | "update" | "delete"; - typename: string; - properties: { - old: Record; - new: Record; - }; - id: Integer | string | number; - timestamp: Integer | string | number; -}; -export type RelationshipSubscriptionMeta = - | RelationshipSubscriptionMetaTypenameParameters - | RelationshipSubscriptionMetaLabelsParameters; -type RelationshipSubscriptionMetaCommonParameters = { - event: "create_relationship" | "delete_relationship"; - relationshipName: string; - id_from: Integer | string | number; - id_to: Integer | string | number; - properties: { - from: Record; - to: Record; - relationship: Record; - }; - id: Integer | string | number; - timestamp: Integer | string | number; -}; -export type RelationshipSubscriptionMetaTypenameParameters = RelationshipSubscriptionMetaCommonParameters & { - fromTypename: string; - toTypename: string; -}; -export type RelationshipSubscriptionMetaLabelsParameters = RelationshipSubscriptionMetaCommonParameters & { - fromLabels: string[]; - toLabels: string[]; -}; -export type EventMeta = NodeSubscriptionMeta | RelationshipSubscriptionMeta; - export type NodeSubscriptionsEvent = | { event: "create"; @@ -480,10 +314,3 @@ export type Neo4jFeaturesSettings = { export type ContextFeatures = Neo4jFeaturesSettings & { subscriptionsEngine?: Neo4jGraphQLSubscriptionsEngine; }; - -export type PredicateReturn = { - predicate: Cypher.Predicate | undefined; - preComputedSubqueries?: Cypher.CompositeClause | undefined; -}; - -export type CypherFieldReferenceMap = Record; diff --git a/packages/graphql/src/utils/compile-cypher.ts b/packages/graphql/src/utils/compile-cypher.ts deleted file mode 100644 index 4a38ee5fb4..0000000000 --- a/packages/graphql/src/utils/compile-cypher.ts +++ /dev/null @@ -1,35 +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 Cypher from "@neo4j/cypher-builder"; - -/** Compiles the cypher of an element, if the resulting cypher is not empty adds a prefix */ -export function compileCypherIfExists( - element: Cypher.Expr | Cypher.Clause | undefined, - env: Cypher.RawCypherContext -): string { - if (!element) return ""; - return compileCypher(element, env); -} - -/** Compiles the cypher of an element, if the resulting cypher is not empty adds a prefix */ -export function compileCypher(element: Cypher.Expr | Cypher.Clause | undefined, env: Cypher.RawCypherContext): string { - if (!element) throw new Error("Cypher element is not defined"); - return env.compile(element); -} diff --git a/packages/graphql/src/utils/find-conflicting-properties.ts b/packages/graphql/src/utils/find-conflicting-properties.ts index f96f34f5e2..9173c3d311 100644 --- a/packages/graphql/src/utils/find-conflicting-properties.ts +++ b/packages/graphql/src/utils/find-conflicting-properties.ts @@ -17,41 +17,33 @@ * limitations under the License. */ -import type { GraphElement } from "../classes"; +import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; import { parseMutationField } from "../translate/queryAST/factory/parsers/parse-mutation-field"; -import mapToDbProperty from "./map-to-db-property"; -/* returns conflicting mutation input properties */ -export function findConflictingProperties({ - graphElement, - input, -}: { - graphElement: GraphElement; - input: Record | undefined; -}): string[] { - if (!input) { - return []; - } - const dbPropertiesToInputFieldNames: Record = Object.keys(input).reduce((acc, rawField) => { - const { fieldName } = parseMutationField(rawField); +export function findConflictingAttributes( + fields: string[], + entityOrRel: ConcreteEntityAdapter | RelationshipAdapter +): Set { + const fieldsByDbName = new Map(); - const dbName = mapToDbProperty(graphElement, fieldName); - // some input fields (eg relation fields) have no corresponding db name in the map - if (!dbName) { - return acc; - } - if (acc[dbName]) { - acc[dbName].push(rawField); - } else { - acc[dbName] = [rawField]; + for (const rawField of fields) { + const { fieldName } = parseMutationField(rawField); + const dbName = entityOrRel.findAttribute(fieldName)?.databaseName; + if (dbName) { + const duplicateFields = fieldsByDbName.get(dbName) ?? []; + duplicateFields.push(rawField); + fieldsByDbName.set(dbName, duplicateFields); } - return acc; - }, {}); + } - return Object.values(dbPropertiesToInputFieldNames) - .filter((v) => v.length > 1) - .reduce((acc, el) => { - acc.push(...el); - return acc; - }, []); + const conflictingAttributes = new Set(); + for (const dedupedProps of fieldsByDbName.values()) { + if (dedupedProps.length > 1) { + for (const fieldName of dedupedProps) { + conflictingAttributes.add(fieldName); + } + } + } + return conflictingAttributes; } diff --git a/packages/graphql/src/utils/get-entity-adapter-from-node.ts b/packages/graphql/src/utils/get-entity-adapter-from-node.ts deleted file mode 100644 index f0bc666b3c..0000000000 --- a/packages/graphql/src/utils/get-entity-adapter-from-node.ts +++ /dev/null @@ -1,30 +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 { EntityAdapter } from "../schema-model/entity/EntityAdapter"; -import type { Node } from "../types"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; - -export function getEntityAdapterFromNode(node: Node, context: Neo4jGraphQLTranslationContext): EntityAdapter { - const entity = context.schemaModel.getConcreteEntityAdapter(node.name); - if (!entity) { - throw new Error(`Could not find entity for node ${node.name}`); - } - return entity; -} diff --git a/packages/graphql/src/utils/get-relationship-type.ts b/packages/graphql/src/utils/get-relationship-type.ts deleted file mode 100644 index f121407dde..0000000000 --- a/packages/graphql/src/utils/get-relationship-type.ts +++ /dev/null @@ -1,26 +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 { ContextFeatures, RelationField } from "../types"; - -export function getRelationshipType(relationField: RelationField, features: ContextFeatures): string { - return features.unsafeEscapeOptions?.disableRelationshipTypeEscaping - ? relationField.typeUnescaped - : relationField.type; -} diff --git a/packages/graphql/src/utils/map-to-db-property.ts b/packages/graphql/src/utils/map-to-db-property.ts deleted file mode 100644 index c01e8dcb5b..0000000000 --- a/packages/graphql/src/utils/map-to-db-property.ts +++ /dev/null @@ -1,30 +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 { GraphElement } from "../classes"; - -// For the @alias directive to be used -function mapToDbProperty(item: GraphElement, graphQLField: string): string { - const itemProp = item.primitiveFields - .concat(item.temporalFields, item.pointFields) - .find(({ fieldName }) => fieldName === graphQLField); - return itemProp?.dbPropertyNameUnescaped || graphQLField; -} - -export default mapToDbProperty; diff --git a/packages/graphql/src/utils/wrap-string-in-apostrophes.ts b/packages/graphql/src/utils/wrap-string-in-apostrophes.ts deleted file mode 100644 index add8ec9ca0..0000000000 --- a/packages/graphql/src/utils/wrap-string-in-apostrophes.ts +++ /dev/null @@ -1,30 +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. - */ - -export function wrapStringInApostrophes(value: string[]): string[]; -export function wrapStringInApostrophes(value: string): string; -export function wrapStringInApostrophes(value: unknown): unknown { - if (typeof value === "string") { - return `'${value}'`; - } - if (Array.isArray(value)) { - return value.map((str) => `'${str}'`); - } - throw new Error("wrapStringInApostrophes can only be used on strings or arrays of strings"); -} diff --git a/packages/graphql/tests/integration/aggregations/where/authorization-with-aggregation-filter.int.test.ts b/packages/graphql/tests/integration/aggregations/where/authorization-with-aggregation-filter.int.test.ts index e3b0448d99..584dbacdde 100644 --- a/packages/graphql/tests/integration/aggregations/where/authorization-with-aggregation-filter.int.test.ts +++ b/packages/graphql/tests/integration/aggregations/where/authorization-with-aggregation-filter.int.test.ts @@ -211,10 +211,7 @@ describe("Authorization with aggregation filter rule", () => { ]); }); - // Test disabled due to flakyness. Enable once `validatePredicate` has been removed from update operations. - // The flakyness is caused by the `AND` operation, that doesn't guarantee shortcircuit of each predicate - // eslint-disable-next-line jest/no-disabled-tests - test.skip("should authorize update operations on post with exactly two likes", async () => { + test("should authorize update operations on post with exactly two likes", async () => { const typeDefs = /* GraphQL */ ` type ${User} @node { id: ID! diff --git a/packages/graphql/tests/integration/array-methods/array-pop-errors.int.test.ts b/packages/graphql/tests/integration/array-methods/array-pop-errors.int.test.ts index 7a74a84cbc..c80a4bc5c6 100644 --- a/packages/graphql/tests/integration/array-methods/array-pop-errors.int.test.ts +++ b/packages/graphql/tests/integration/array-methods/array-pop-errors.int.test.ts @@ -77,54 +77,6 @@ describe("array-pop-errors", () => { expect(gqlResult.data).toBeNull(); }); - test("should throw an error when trying to pop an element from multiple non-existing arrays", async () => { - const typeMovie = testHelper.createUniqueType("Movie"); - - const typeDefs = gql` - type ${typeMovie} @node { - title: String - tags: [String!] - otherTags: [String!] - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const update = ` - mutation { - ${typeMovie.operations.update} (update: { tags_POP: 1, otherTags_POP: 1 }) { - ${typeMovie.plural} { - title - tags - otherTags - } - } - } - `; - - // Created deliberately without the tags or otherTags properties. - const cypher = ` - CREATE (m:${typeMovie} {title:$movieTitle}) - `; - - await testHelper.executeCypher(cypher, { movieTitle }); - - const gqlResult = await testHelper.executeGraphQL(update); - - expect(gqlResult.errors).toBeDefined(); - expect( - (gqlResult.errors as GraphQLError[]).some((el) => - el.message.includes("Properties tags, otherTags cannot be NULL") - ) - ).toBeTruthy(); - - expect(gqlResult.data).toBeNull(); - }); - test("should throw an error if not authenticated on field definition", async () => { const typeMovie = testHelper.createUniqueType("Movie"); const typeDefs = ` @@ -335,9 +287,8 @@ describe("array-pop-errors", () => { expect(gqlResult.errors).toBeDefined(); - const relationshipType = `${movie.name}ActorsRelationship`; expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_POP]] on type ${relationshipType}`), + new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_POP]] on type ${actor}.actedIn`), ]); expect(gqlResult.data).toBeNull(); }); diff --git a/packages/graphql/tests/integration/array-methods/array-push-errors.int.test.ts b/packages/graphql/tests/integration/array-methods/array-push-errors.int.test.ts index 030fab8834..7a24fbd366 100644 --- a/packages/graphql/tests/integration/array-methods/array-push-errors.int.test.ts +++ b/packages/graphql/tests/integration/array-methods/array-push-errors.int.test.ts @@ -286,9 +286,8 @@ describe("array-push", () => { }); expect(gqlResult.errors).toBeDefined(); - const relationshipType = `${movie.name}ActorsRelationship`; expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_PUSH]] on type ${relationshipType}`), + new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_PUSH]] on type ${actor}.actedIn`), ]); expect(gqlResult.data).toBeNull(); diff --git a/packages/graphql/tests/integration/array-methods/array-push.int.test.ts b/packages/graphql/tests/integration/array-methods/array-push.int.test.ts index 16730b31b5..34253a1092 100644 --- a/packages/graphql/tests/integration/array-methods/array-push.int.test.ts +++ b/packages/graphql/tests/integration/array-methods/array-push.int.test.ts @@ -40,184 +40,219 @@ describe("array-push", () => { const expectedLocalDateTime = expect.stringContaining(localDateTime); test.each([ - { description: "a single Int element", inputType: "Int", inputValue: 100, expectedOutputValue: [100] }, + { + description: "a single Int element", + inputType: "Int", + inputValue: 100, + expectedOutputValue: [1, 100], + initialArray: [1], + }, { description: "a single Int element in an array", inputType: "Int", inputValue: `[${100}]`, - expectedOutputValue: [100], + expectedOutputValue: [1, 100], + initialArray: [1], }, { description: "multiple Int elements", inputType: "Int", inputValue: `[${100}, ${100}]`, - expectedOutputValue: [100, 100], + expectedOutputValue: [1, 100, 100], + initialArray: [1], }, { description: "a single Float element", inputType: "Float", inputValue: 0.123456, - expectedOutputValue: [0.123456], + expectedOutputValue: [1.1, 0.123456], + initialArray: [1.1], }, { description: "a single Float element in an array", inputType: "Float", inputValue: `[${0.123456}]`, - expectedOutputValue: [0.123456], + expectedOutputValue: [1.1, 0.123456], + initialArray: [1.1], }, { description: "multiple Float elements", inputType: "Float", inputValue: `[${0.123456}, ${0.123456}]`, - expectedOutputValue: [0.123456, 0.123456], + expectedOutputValue: [1.1, 0.123456, 0.123456], + initialArray: [1.1], }, { description: "a single String element", inputType: "String", inputValue: `"tag"`, - expectedOutputValue: ["tag"], + expectedOutputValue: ["first", "tag"], + initialArray: ["first"], }, { description: "a single String element in an array", inputType: "String", inputValue: `["tag"]`, - expectedOutputValue: ["tag"], + expectedOutputValue: ["first", "tag"], + initialArray: ["first"], }, { description: "multiple String elements", inputType: "String", inputValue: `["tag1", "tag2"]`, - expectedOutputValue: ["tag1", "tag2"], + expectedOutputValue: ["first", "tag1", "tag2"], + initialArray: ["first"], }, { description: "a single Boolean element", inputType: "Boolean", inputValue: true, - expectedOutputValue: [true], + expectedOutputValue: [true, true], + initialArray: [true], }, { description: "a single Boolean element in an array", inputType: "Boolean", inputValue: `[${true}]`, - expectedOutputValue: [true], + expectedOutputValue: [true, true], + initialArray: [true], }, { description: "multiple Boolean elements", inputType: "Boolean", inputValue: `[${false}, ${true}]`, - expectedOutputValue: [false, true], + expectedOutputValue: [true, false, true], + initialArray: [true], }, { description: "a single Duration element", inputType: "Duration", inputValue: `"P2MT10S"`, expectedOutputValue: ["P2MT10S"], + initialArray: [], }, { description: "a single Duration element in an array", inputType: "Duration", inputValue: `["P2MT10S"]`, expectedOutputValue: ["P2MT10S"], + initialArray: [], }, { description: "multiple Duration elements", inputType: "Duration", inputValue: `["P2MT10S", "P2MT10S"]`, expectedOutputValue: ["P2MT10S", "P2MT10S"], + initialArray: [], }, { description: "a single Date element", inputType: "Date", inputValue: `"${date}"`, expectedOutputValue: [expectedDateOutput], + initialArray: [], }, { description: "a single Date element in an array", inputType: "Date", inputValue: `["${date}"]`, expectedOutputValue: [expectedDateOutput], + initialArray: [], }, { description: "multiple Date elements", inputType: "Date", inputValue: `["${date}", "${date}"]`, expectedOutputValue: [expectedDateOutput, expectedDateOutput], + initialArray: [], }, { description: "a single Time element", inputType: "Time", inputValue: `"${time}"`, expectedOutputValue: [expectedTimeOutput], + initialArray: [], }, { description: "a single Time element in an array", inputType: "Time", inputValue: `["${time}"]`, expectedOutputValue: [expectedTimeOutput], + initialArray: [], }, { description: "multiple Time elements", inputType: "Time", inputValue: `["${time}", "${time}"]`, expectedOutputValue: [expectedTimeOutput, expectedTimeOutput], + initialArray: [], }, { description: "a single LocalTime element", inputType: "LocalTime", inputValue: `"${localTime}"`, expectedOutputValue: [expectedLocalTime], + initialArray: [], }, { description: "a single LocalTime element in an array", inputType: "LocalTime", inputValue: `["${localTime}"]`, expectedOutputValue: [expectedLocalTime], + initialArray: [], }, { description: "multiple LocalTime elements", inputType: "LocalTime", inputValue: `["${localTime}", "${localTime}"]`, expectedOutputValue: [expectedLocalTime, expectedLocalTime], + initialArray: [], }, { description: "a single DateTime element", inputType: "DateTime", inputValue: `"${date}"`, expectedOutputValue: [date], + initialArray: [], }, { description: "a single DateTime element in an array", inputType: "DateTime", inputValue: `["${date}"]`, expectedOutputValue: [date], + initialArray: [], }, { description: "multiple DateTime elements", inputType: "DateTime", inputValue: `["${date}", "${date}"]`, expectedOutputValue: [date, date], + initialArray: [], }, { description: "a single LocalDateTime element", inputType: "LocalDateTime", inputValue: `"${localDateTime}"`, expectedOutputValue: [expectedLocalDateTime], + initialArray: [], }, { description: "a single LocalDateTime element in an array", inputType: "LocalDateTime", inputValue: `["${localDateTime}"]`, expectedOutputValue: [expectedLocalDateTime], + initialArray: [], }, { description: "multiple LocalDateTime elements", inputType: "LocalDateTime", inputValue: `["${localDateTime}", "${localDateTime}"]`, expectedOutputValue: [expectedLocalDateTime, expectedLocalDateTime], + initialArray: [], }, ] as const)( "should push $description on to an existing array", - async ({ inputType, inputValue, expectedOutputValue }) => { + async ({ initialArray, inputType, inputValue, expectedOutputValue }) => { const typeMovie = testHelper.createUniqueType("Movie"); const typeDefs = gql` @@ -245,10 +280,10 @@ describe("array-push", () => { `; const cypher = ` - CREATE (m:${typeMovie} {title:$movieTitle, tags: []}) + CREATE (m:${typeMovie} {title:$movieTitle, tags: $initialArray}) `; - await testHelper.executeCypher(cypher, { movieTitle }); + await testHelper.executeCypher(cypher, { movieTitle, initialArray }); const gqlResult = await testHelper.executeGraphQL(update); diff --git a/packages/graphql/tests/integration/deprecations/aggregations/where/authorization-with-aggregation-filter.int.test.ts b/packages/graphql/tests/integration/deprecations/aggregations/where/authorization-with-aggregation-filter.int.test.ts index 8dcbc216c7..1090cce934 100644 --- a/packages/graphql/tests/integration/deprecations/aggregations/where/authorization-with-aggregation-filter.int.test.ts +++ b/packages/graphql/tests/integration/deprecations/aggregations/where/authorization-with-aggregation-filter.int.test.ts @@ -194,10 +194,7 @@ describe("authorization-with-aggregation-filter", () => { expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); }); - // Test disabled due to flakyness. Enable once `validatePredicate` has been removed from update operations. - // The flakyness is caused by the `AND` operation, that doesn't guarantee shortcircuit of each predicate - // eslint-disable-next-line jest/no-disabled-tests - test.skip("should authorize update operations on post with exactly two likes", async () => { + test("should authorize update operations on post with exactly two likes", async () => { const typeDefs = /* GraphQL */ ` type ${User} @node { id: ID! diff --git a/packages/graphql/tests/integration/deprecations/math-deprecated.int.test.ts b/packages/graphql/tests/integration/deprecations/math-deprecated.int.test.ts index b47e239b0a..dc0f703833 100644 --- a/packages/graphql/tests/integration/deprecations/math-deprecated.int.test.ts +++ b/packages/graphql/tests/integration/deprecations/math-deprecated.int.test.ts @@ -493,7 +493,7 @@ describe("Mathematical operations tests", () => { expect(gqlResult.errors).toBeDefined(); expect( (gqlResult.errors as GraphQLError[]).some((el) => - el.message.includes(`Cannot _INCREMENT ${increment} to Nan`) + el.message.includes(`Cannot increment ${increment} to Nan`) ) ).toBeTruthy(); const storedValue = await testHelper.executeCypher( @@ -670,9 +670,8 @@ describe("Mathematical operations tests", () => { expect(gqlResult.errors).toBeDefined(); - const relationshipType = `${movie.name}ActorsRelationship`; expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_ADD]] on type ${relationshipType}`), + new GraphQLError(`Conflicting modification of [[pay_SET]], [[pay_ADD]] on type ${actor}.actedIn`), ]); const storedValue = await testHelper.executeCypher( ` diff --git a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts index 93403f9dbc..9ca47d197c 100644 --- a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts +++ b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts @@ -335,7 +335,9 @@ describe("@alias directive", () => { expect(gqlResult.errors).toBeDefined(); expect(gqlResult.errors).toHaveLength(1); expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}`), + new GraphQLError( + `Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}` + ), ]); expect(gqlResult?.data?.[typeDirector.operations.update]?.[typeDirector.plural]).toBeUndefined(); @@ -389,7 +391,9 @@ describe("@alias directive", () => { expect(gqlResult.errors).toBeDefined(); expect(gqlResult.errors).toHaveLength(1); expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}`), + new GraphQLError( + `Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}` + ), ]); expect(gqlResult?.data?.[typeDirector.operations.update]?.[typeDirector.plural]).toBeUndefined(); @@ -594,7 +598,9 @@ describe("@alias directive", () => { expect(gqlResult.errors).toBeDefined(); expect(gqlResult.errors).toHaveLength(1); expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}`), + new GraphQLError( + `Conflicting modification of [[name_SET]], [[nameAgain_SET]] on type ${typeDirector.name}` + ), ]); expect(gqlResult?.data?.[typeDirector.operations.update]?.[typeDirector.plural]).toBeUndefined(); diff --git a/packages/graphql/tests/integration/directives/authorization/bind.int.test.ts b/packages/graphql/tests/integration/directives/authorization/bind.int.test.ts index 01f1494cc9..ed8e681002 100644 --- a/packages/graphql/tests/integration/directives/authorization/bind.int.test.ts +++ b/packages/graphql/tests/integration/directives/authorization/bind.int.test.ts @@ -636,6 +636,7 @@ describe("auth/bind", () => { await testHelper.executeCypher(` CREATE (:${Post} {id: "${postId}"}) + CREATE (:${User} {id: "not bound"}) `); const token = createBearerToken(secret, { sub: userId }); diff --git a/packages/graphql/tests/integration/directives/customResolver.int.test.ts b/packages/graphql/tests/integration/directives/customResolver.int.test.ts index aaceaec113..f4a7067d3c 100644 --- a/packages/graphql/tests/integration/directives/customResolver.int.test.ts +++ b/packages/graphql/tests/integration/directives/customResolver.int.test.ts @@ -18,7 +18,7 @@ */ import gql from "graphql-tag"; -import { INVALID_REQUIRED_FIELD_ERROR } from "../../../src/schema/get-custom-resolver-meta"; +import { INVALID_REQUIRED_FIELD_ERROR } from "../../../src/schema/selection-set-to-resolve-tree"; import { createBearerToken } from "../../utils/create-bearer-token"; import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; diff --git a/packages/graphql/tests/integration/directives/populatedBy/populatedBy-node-properties.int.test.ts b/packages/graphql/tests/integration/directives/populatedBy/populatedBy-node-properties.int.test.ts index d03c615c8d..bd8b811fc2 100644 --- a/packages/graphql/tests/integration/directives/populatedBy/populatedBy-node-properties.int.test.ts +++ b/packages/graphql/tests/integration/directives/populatedBy/populatedBy-node-properties.int.test.ts @@ -1811,7 +1811,7 @@ describe("@populatedBy directive - Node properties", () => { [testMovie.plural]: [ { id: movieId, - callback: `${date.toISOString().split("T")[1]?.split("Z")[0]}Z`, + callback: `${date.toISOString().split("T")[1]?.split("Z")[0]}000000Z`, }, ], }, diff --git a/packages/graphql/tests/integration/directives/populatedBy/populatedBy-relationship-properties.int.test.ts b/packages/graphql/tests/integration/directives/populatedBy/populatedBy-relationship-properties.int.test.ts index e0d4cf6082..a75f56ef56 100644 --- a/packages/graphql/tests/integration/directives/populatedBy/populatedBy-relationship-properties.int.test.ts +++ b/packages/graphql/tests/integration/directives/populatedBy/populatedBy-relationship-properties.int.test.ts @@ -2441,7 +2441,6 @@ describe("@populatedBy directive - Relationship properties", () => { type: "Time", callback: () => Promise.resolve(`${date.toISOString().split("T")[1]}`), expectedValue: `${date.toISOString().split("T")[1]?.split("Z")[0]}000000Z`, - expectedValueTemp: `${date.toISOString().split("T")[1]?.split("Z")[0]}Z`, // TODO: this is a due to a bug with custom input objects, only used for Update until this is moved to QueryAST }, { description: "@populatedBy - LocalDateTime", @@ -3208,7 +3207,6 @@ describe("@populatedBy directive - Relationship properties", () => { const testMovie = testHelper.createUniqueType("Movie"); const testGenre = testHelper.createUniqueType("Genre"); const callback = (parent) => `${parent.title_SET}-slug`; - const typeDefs = /* GraphQL */ ` type ${testMovie.name} @node { id: ID @@ -3286,7 +3284,6 @@ describe("@populatedBy directive - Relationship properties", () => { `); const result = await testHelper.executeGraphQL(mutation); - expect(result.errors).toBeUndefined(); expect(result.data as any).toMatchObject({ [testMovie.operations.update]: { diff --git a/packages/graphql/tests/integration/interfaces/relationships/create/connect.int.test.ts b/packages/graphql/tests/integration/interfaces/relationships/create/connect.int.test.ts index 19b23fe110..abd83e017b 100644 --- a/packages/graphql/tests/integration/interfaces/relationships/create/connect.int.test.ts +++ b/packages/graphql/tests/integration/interfaces/relationships/create/connect.int.test.ts @@ -123,6 +123,10 @@ describe("interface relationships", () => { } } } + info { + nodesCreated + relationshipsCreated + } } } `; @@ -160,6 +164,10 @@ describe("interface relationships", () => { name: actorName1, }, ], + info: { + nodesCreated: 1, + relationshipsCreated: 2, + }, }, }); }); diff --git a/packages/graphql/tests/integration/interfaces/relationships/declare-relationship/interface-simple.int.test.ts b/packages/graphql/tests/integration/interfaces/relationships/declare-relationship/interface-simple.int.test.ts index 962b13135a..cc617c70c9 100644 --- a/packages/graphql/tests/integration/interfaces/relationships/declare-relationship/interface-simple.int.test.ts +++ b/packages/graphql/tests/integration/interfaces/relationships/declare-relationship/interface-simple.int.test.ts @@ -1130,7 +1130,6 @@ describe("interface with declared relationships", () => { const gqlResult = await testHelper.executeGraphQL(query); expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data?.[Actor.operations.update] as Record)?.[Actor.plural]).toIncludeSameMembers( [ { diff --git a/packages/graphql/tests/integration/issues/3929.int.test.ts b/packages/graphql/tests/integration/issues/3929.int.test.ts index 06dbbf9fa7..23bfe05b75 100644 --- a/packages/graphql/tests/integration/issues/3929.int.test.ts +++ b/packages/graphql/tests/integration/issues/3929.int.test.ts @@ -53,7 +53,7 @@ describe("https://github.com/neo4j/graphql/issues/3929", () => { creator: [${User}!]! @relationship(type: "CREATOR_OF", direction: IN, nestedOperations: [CONNECT]) } - type ${Person} @authorization(validate: [{ where: { node: { creator_SINGLE: { id_EQ: "$jwt.uid" }}}}]) @node { + type ${Person} @authorization(validate: [{ where: { node: { creator_SINGLE: { id_EQ: "$jwt.uid" }}}}]) @node { id: ID! @id name: String! creator: [${User}!]! @relationship(type: "CREATOR_OF", direction: IN) diff --git a/packages/graphql/tests/integration/math.int.test.ts b/packages/graphql/tests/integration/math.int.test.ts index 70df9ee16c..6f9d88cd7a 100644 --- a/packages/graphql/tests/integration/math.int.test.ts +++ b/packages/graphql/tests/integration/math.int.test.ts @@ -493,7 +493,7 @@ describe("Mathematical operations tests", () => { expect(gqlResult.errors).toBeDefined(); expect( (gqlResult.errors as GraphQLError[]).some((el) => - el.message.includes(`Cannot _INCREMENT ${increment} to Nan`) + el.message.includes(`Cannot increment ${increment} to Nan`) ) ).toBeTruthy(); const storedValue = await testHelper.executeCypher( @@ -669,9 +669,10 @@ describe("Mathematical operations tests", () => { expect(gqlResult.errors).toBeDefined(); - const relationshipType = `${movie.name}ActorsRelationship`; expect(gqlResult.errors).toEqual([ - new GraphQLError(`Conflicting modification of field pay: [[set]], [[add]] on type ${relationshipType}`), + new GraphQLError( + `Conflicting modification of field pay: [[set]], [[add]] on relationship ${movie.name}.actedIn` + ), ]); const storedValue = await testHelper.executeCypher( ` diff --git a/packages/graphql/tests/integration/unions/unions.int.test.ts b/packages/graphql/tests/integration/unions/unions.int.test.ts index da2ebf9914..3ae5393ca5 100644 --- a/packages/graphql/tests/integration/unions/unions.int.test.ts +++ b/packages/graphql/tests/integration/unions/unions.int.test.ts @@ -715,10 +715,12 @@ describe("unions", () => { const gqlResult = await testHelper.executeGraphQL(mutation); expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[MovieType.operations.update][MovieType.plural][0]).toMatchObject({ - title: movieTitle, - search: [], + expect(gqlResult.data).toEqual({ + [MovieType.operations.update]: { + [MovieType.plural]: [{ title: movieTitle, search: [] }], + }, }); + await testHelper.expectRelationship(MovieType, GenreType, "SEARCH").toNotExist(); }); describe("Unions with auth", () => { diff --git a/packages/graphql/tests/integration/update.int.test.ts b/packages/graphql/tests/integration/update.int.test.ts index 87c6e3ba26..bc165ffd2d 100644 --- a/packages/graphql/tests/integration/update.int.test.ts +++ b/packages/graphql/tests/integration/update.int.test.ts @@ -212,17 +212,43 @@ describe("update", () => { expect(gqlResult.errors).toBeFalsy(); - const cypherResult = await testHelper.executeCypher( - ` - MATCH (p:${Person} {id: "4"})-[:DIRECTED]->(m:${Movie} {id: "5"}) RETURN p, m - ` + expect(gqlResult.data).toEqual({ + [Movie.operations.update]: { + [Movie.plural]: [{ id: "1", title: "Movie1" }], + }, + }); + await testHelper.expectRelationship(Person, Movie, "DIRECTED").toEqual( + expect.toIncludeSameMembers([ + { + from: { + id: "2", + }, + to: { id: "1", title: "Movie1" }, + relationship: {}, + }, + { + from: { + id: "2", + }, + to: { id: "3", title: "Movie3" }, + relationship: {}, + }, + { + from: { + id: "4", + }, + to: { id: "3", title: "Movie3" }, + relationship: {}, + }, + { + from: { + id: "4", + }, + to: { id: "5", title: "Movie5" }, + relationship: {}, + }, + ]) ); - - expect(cypherResult.records).toHaveLength(2); - - expect(gqlResult?.data?.[Movie.operations.update]).toEqual({ - [Movie.plural]: [{ id: "1", title: "Movie1" }], - }); }); test("should update a movie when matching on relationship property", async () => { @@ -362,17 +388,11 @@ describe("update", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); - const movieId = generate({ - charset: "alphabetic", - }); + const movieId = "movieId"; - const initialName = generate({ - charset: "alphabetic", - }); + const initialName = "Original Name"; - const updatedName = generate({ - charset: "alphabetic", - }); + const updatedName = "New Fancy Name"; const query = /* GraphQL */ ` mutation($movieId: ID, $initialName: String, $updatedName: String) { diff --git a/packages/graphql/tests/tck/array-methods.test.ts b/packages/graphql/tests/tck/array-methods.test.ts index e1f7fd2ba6..c4707e2f22 100644 --- a/packages/graphql/tests/tck/array-methods.test.ts +++ b/packages/graphql/tests/tck/array-methods.test.ts @@ -50,18 +50,19 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) + SET + this.ratings = (this.ratings + $param0) WITH this - WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property %s cannot be NULL\\", ['ratings']) - SET this.ratings = this.ratings + $this_update_ratings_PUSH - RETURN collect(DISTINCT this { .title, .ratings }) AS data" + RETURN this { .title, .ratings } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_PUSH\\": [ + \\"param0\\": [ 1 - ], - \\"resolvedCallbacks\\": {} + ] }" `); }); @@ -96,22 +97,23 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE (apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) AND apoc.util.validatePredicate(this.scores IS NULL, \\"Property scores cannot be NULL\\", [0])) + SET + this.ratings = (this.ratings + $param0), + this.scores = (this.scores + $param1) WITH this - WHERE apoc.util.validatePredicate(this.ratings IS NULL OR this.scores IS NULL, \\"Properties %s, %s cannot be NULL\\", ['ratings', 'scores']) - SET this.ratings = this.ratings + $this_update_ratings_PUSH - SET this.scores = this.scores + $this_update_scores_PUSH - RETURN collect(DISTINCT this { .title, .ratings, .scores }) AS data" + RETURN this { .title, .ratings, .scores } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_PUSH\\": [ + \\"param0\\": [ 1 ], - \\"this_update_scores_PUSH\\": [ + \\"param1\\": [ 1 - ], - \\"resolvedCallbacks\\": {} + ] }" `); }); @@ -156,22 +158,23 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE apoc.util.validatePredicate(this.filmingLocations IS NULL, \\"Property filmingLocations cannot be NULL\\", [0]) + SET + this.filmingLocations = (this.filmingLocations + [var0 IN $param0 | point(var0)]) WITH this - WHERE apoc.util.validatePredicate(this.filmingLocations IS NULL, \\"Property %s cannot be NULL\\", ['filmingLocations']) - SET this.filmingLocations = this.filmingLocations + [p in $this_update_filmingLocations_PUSH | point(p)] - RETURN collect(DISTINCT this { .title, .filmingLocations }) AS data" + RETURN this { .title, .filmingLocations } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_filmingLocations_PUSH\\": [ + \\"param0\\": [ { \\"longitude\\": -178.7374, \\"latitude\\": 38.4554, \\"height\\": 60111.54 } - ], - \\"resolvedCallbacks\\": {} + ] }" `); }); @@ -214,26 +217,30 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.ratings = (this.ratings + $param3) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(this.ratings IS NULL, \\"Property %s cannot be NULL\\", ['ratings']) - SET this.ratings = this.ratings + $this_update_ratings_PUSH - WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .title, .ratings }) AS data" + RETURN this { .title, .ratings } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_PUSH\\": [ - 1 - ], \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [] }, - \\"authorization__before_param2\\": \\"update\\", - \\"authorization__after_param2\\": \\"update\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"update\\", + \\"param3\\": [ + 1 + ], + \\"param4\\": \\"update\\" }" `); }); @@ -266,19 +273,20 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) + SET + this.ratings = this.ratings[0..-$param0] WITH this - WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property %s cannot be NULL\\", ['ratings']) - SET this.ratings = this.ratings[0..-$this_update_ratings_POP] - RETURN collect(DISTINCT this { .title, .ratings }) AS data" + RETURN this { .title, .ratings } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_POP\\": { + \\"param0\\": { \\"low\\": 1, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -313,24 +321,25 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE (apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) AND apoc.util.validatePredicate(this.scores IS NULL, \\"Property scores cannot be NULL\\", [0])) + SET + this.ratings = this.ratings[0..-$param0], + this.scores = this.scores[0..-$param1] WITH this - WHERE apoc.util.validatePredicate(this.ratings IS NULL OR this.scores IS NULL, \\"Properties %s, %s cannot be NULL\\", ['ratings', 'scores']) - SET this.ratings = this.ratings[0..-$this_update_ratings_POP] - SET this.scores = this.scores[0..-$this_update_scores_POP] - RETURN collect(DISTINCT this { .title, .ratings, .scores }) AS data" + RETURN this { .title, .ratings, .scores } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_POP\\": { + \\"param0\\": { \\"low\\": 1, \\"high\\": 0 }, - \\"this_update_scores_POP\\": { + \\"param1\\": { \\"low\\": 1, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -373,27 +382,31 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.ratings = this.ratings[0..-$param3] + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(this.ratings IS NULL, \\"Property %s cannot be NULL\\", ['ratings']) - SET this.ratings = this.ratings[0..-$this_update_ratings_POP] - WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .title, .ratings }) AS data" + RETURN this { .title, .ratings } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_POP\\": { - \\"low\\": 1, - \\"high\\": 0 - }, \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [] }, - \\"authorization__before_param2\\": \\"update\\", - \\"authorization__after_param2\\": \\"update\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"update\\", + \\"param3\\": { + \\"low\\": 1, + \\"high\\": 0 + }, + \\"param4\\": \\"update\\" }" `); }); @@ -428,23 +441,24 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WHERE (apoc.util.validatePredicate(this.ratings IS NULL, \\"Property ratings cannot be NULL\\", [0]) AND apoc.util.validatePredicate(this.scores IS NULL, \\"Property scores cannot be NULL\\", [0])) + SET + this.ratings = (this.ratings + $param0), + this.scores = this.scores[0..-$param1] WITH this - WHERE apoc.util.validatePredicate(this.ratings IS NULL OR this.scores IS NULL, \\"Properties %s, %s cannot be NULL\\", ['ratings', 'scores']) - SET this.ratings = this.ratings + $this_update_ratings_PUSH - SET this.scores = this.scores[0..-$this_update_scores_POP] - RETURN collect(DISTINCT this { .title, .ratings, .scores }) AS data" + RETURN this { .title, .ratings, .scores } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_ratings_PUSH\\": [ + \\"param0\\": [ 1.5 ], - \\"this_update_scores_POP\\": { + \\"param1\\": { \\"low\\": 1, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -496,55 +510,42 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - SET this_acted_in0_relationship.pay = this_acted_in0_relationship.pay + $updateActors.args.update.actedIn[0].update.edge.pay_PUSH - RETURN count(*) AS update_this_actedIn0 - } WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + WHERE apoc.util.validatePredicate(this0.pay IS NULL, \\"Property pay cannot be NULL\\", [0]) + SET + this0.pay = (this0.pay + $param1) + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .title } AS this3 + RETURN collect(this3) AS var4 } CALL (this) { - MATCH (this)-[update_this3:ACTED_IN]->(update_this4:Movie) - WITH collect({ node: update_this4, relationship: update_this3 }) AS edges + MATCH (this)-[this5:ACTED_IN]->(this6:Movie) + WITH collect({ node: this6, relationship: this5 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this4, edge.relationship AS update_this3 - RETURN collect({ properties: { pay: update_this3.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(update_this4), __resolveType: \\"Movie\\" } }) AS update_var5 + WITH edge.node AS this6, edge.relationship AS this5 + RETURN collect({ properties: { pay: this5.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(this6), __resolveType: \\"Movie\\" } }) AS var7 } - RETURN { edges: update_var5 } AS update_var6 + RETURN { edges: var7 } AS var8 } - RETURN collect(DISTINCT this { .name, actedIn: update_var2, actedInConnection: update_var6 }) AS data" + RETURN this { .name, actedIn: var4, actedInConnection: var8 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"update\\": { - \\"edge\\": { - \\"pay_PUSH\\": [ - 10 - ] - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": [ + 10 + ] }" `); }); @@ -596,56 +597,43 @@ describe("Arrays Methods", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - SET this_acted_in0_relationship.pay = this_acted_in0_relationship.pay[0..-$updateActors.args.update.actedIn[0].update.edge.pay_POP] - RETURN count(*) AS update_this_actedIn0 - } WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + WHERE apoc.util.validatePredicate(this0.pay IS NULL, \\"Property pay cannot be NULL\\", [0]) + SET + this0.pay = this0.pay[0..-$param1] + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .title } AS this3 + RETURN collect(this3) AS var4 } CALL (this) { - MATCH (this)-[update_this3:ACTED_IN]->(update_this4:Movie) - WITH collect({ node: update_this4, relationship: update_this3 }) AS edges + MATCH (this)-[this5:ACTED_IN]->(this6:Movie) + WITH collect({ node: this6, relationship: this5 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this4, edge.relationship AS update_this3 - RETURN collect({ properties: { pay: update_this3.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(update_this4), __resolveType: \\"Movie\\" } }) AS update_var5 + WITH edge.node AS this6, edge.relationship AS this5 + RETURN collect({ properties: { pay: this5.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(this6), __resolveType: \\"Movie\\" } }) AS var7 } - RETURN { edges: update_var5 } AS update_var6 + RETURN { edges: var7 } AS var8 } - RETURN collect(DISTINCT this { .name, actedIn: update_var2, actedInConnection: update_var6 }) AS data" + RETURN this { .name, actedIn: var4, actedInConnection: var8 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"update\\": { - \\"edge\\": { - \\"pay_POP\\": { - \\"low\\": 1, - \\"high\\": 0 - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": { + \\"low\\": 1, + \\"high\\": 0 + } }" `); }); diff --git a/packages/graphql/tests/tck/connections/projections/update.test.ts b/packages/graphql/tests/tck/connections/projections/update.test.ts index 4ae6b5f4bb..7654d301e9 100644 --- a/packages/graphql/tests/tck/connections/projections/update.test.ts +++ b/packages/graphql/tests/tck/connections/projections/update.test.ts @@ -72,25 +72,25 @@ describe("Cypher -> Connections -> Projections -> Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - WHERE this.title = $param0 WITH * + WHERE this.title = $param0 + WITH this CALL (this) { - MATCH (this)<-[update_this0:ACTED_IN]-(update_this1:Actor) - WITH collect({ node: update_this1, relationship: update_this0 }) AS edges + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH collect({ node: this1, relationship: this0 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this1, edge.relationship AS update_this0 - RETURN collect({ properties: { screenTime: update_this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: update_this1.name, __resolveType: \\"Actor\\" } }) AS update_var2 + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 } - RETURN { edges: update_var2 } AS update_var3 + RETURN { edges: var2 } AS var3 } - RETURN collect(DISTINCT this { .title, actorsConnection: update_var3 }) AS data" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"param0\\": \\"Forrest Gump\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Forrest Gump\\" }" `); }); diff --git a/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts b/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts index 0e2ee08b68..f8b92074d9 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts @@ -218,45 +218,38 @@ describe("Relationship Properties Connect Cypher", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect0_node:Actor) - CALL(*) { - WITH collect(this_actors0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect0_node - CREATE (this)<-[this_actors0_connect0_relationship:ACTED_IN]-(this_actors0_connect0_node) - SET this_actors0_connect0_relationship.screenTime = $this_actors0_connect0_relationship_screenTime - } - } - WITH this, this_actors0_connect0_node - RETURN count(*) AS connect_this_actors0_connect_Actor0 + CALL (*) { + CALL (this) { + MATCH (this0:Actor) + CREATE (this)<-[this1:ACTED_IN]-(this0) + SET + this1.screenTime = $param1 + } } - WITH * + WITH this CALL (this) { - MATCH (this)<-[update_this0:ACTED_IN]-(update_this1:Actor) - WITH collect({ node: update_this1, relationship: update_this0 }) AS edges + MATCH (this)<-[this2:ACTED_IN]-(this3:Actor) + WITH collect({ node: this3, relationship: this2 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this1, edge.relationship AS update_this0 - RETURN collect({ properties: { screenTime: update_this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: update_this1.name, __resolveType: \\"Actor\\" } }) AS update_var2 + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ properties: { screenTime: this2.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this3.name, __resolveType: \\"Actor\\" } }) AS var4 } - RETURN { edges: update_var2 } AS update_var3 + RETURN { edges: var4 } AS var5 } - RETURN collect(DISTINCT this { .title, actorsConnection: update_var3 }) AS data" + RETURN this { .title, actorsConnection: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Forrest Gump\\", - \\"this_actors0_connect0_relationship_screenTime\\": { + \\"param1\\": { \\"low\\": 60, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -294,47 +287,40 @@ describe("Relationship Properties Connect Cypher", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect0_node:Actor) - WHERE this_actors0_connect0_node.name = $this_actors0_connect0_node_param0 - CALL(*) { - WITH collect(this_actors0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect0_node - CREATE (this)<-[this_actors0_connect0_relationship:ACTED_IN]-(this_actors0_connect0_node) - SET this_actors0_connect0_relationship.screenTime = $this_actors0_connect0_relationship_screenTime - } - } - WITH this, this_actors0_connect0_node - RETURN count(*) AS connect_this_actors0_connect_Actor0 + CALL (*) { + CALL (this) { + MATCH (this0:Actor) + WHERE this0.name = $param1 + CREATE (this)<-[this1:ACTED_IN]-(this0) + SET + this1.screenTime = $param2 + } } - WITH * + WITH this CALL (this) { - MATCH (this)<-[update_this0:ACTED_IN]-(update_this1:Actor) - WITH collect({ node: update_this1, relationship: update_this0 }) AS edges + MATCH (this)<-[this2:ACTED_IN]-(this3:Actor) + WITH collect({ node: this3, relationship: this2 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this1, edge.relationship AS update_this0 - RETURN collect({ properties: { screenTime: update_this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: update_this1.name, __resolveType: \\"Actor\\" } }) AS update_var2 + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ properties: { screenTime: this2.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this3.name, __resolveType: \\"Actor\\" } }) AS var4 } - RETURN { edges: update_var2 } AS update_var3 + RETURN { edges: var4 } AS var5 } - RETURN collect(DISTINCT this { .title, actorsConnection: update_var3 }) AS data" + RETURN this { .title, actorsConnection: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Forrest Gump\\", - \\"this_actors0_connect0_node_param0\\": \\"Tom Hanks\\", - \\"this_actors0_connect0_relationship_screenTime\\": { + \\"param1\\": \\"Tom Hanks\\", + \\"param2\\": { \\"low\\": 60, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/connections/relationship_properties/update.test.ts b/packages/graphql/tests/tck/connections/relationship_properties/update.test.ts index 2c60a0bf8a..b48e63f21e 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/update.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/update.test.ts @@ -69,48 +69,28 @@ describe("Cypher -> Connections -> Relationship Properties -> Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_acted_in0_relationship.screenTime = $updateMovies.args.update.actors[0].update.edge.screenTime_SET - RETURN count(*) AS update_this_actors0 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param1 + SET + this0.screenTime = $param2 } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Forrest Gump\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"Tom Hanks\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Tom Hanks\\" - } - } - }, - \\"edge\\": { - \\"screenTime_SET\\": { - \\"low\\": 60, - \\"high\\": 0 - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Tom Hanks\\", + \\"param2\\": { + \\"low\\": 60, + \\"high\\": 0 + } }" `); }); @@ -144,53 +124,30 @@ describe("Cypher -> Connections -> Relationship Properties -> Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_acted_in0_relationship.screenTime = $updateMovies.args.update.actors[0].update.edge.screenTime_SET - SET this_actors0.name = $this_update_actors0_name_SET - RETURN count(*) AS update_this_actors0 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2, + this0.screenTime = $param3 } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Forrest Gump\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"Tom Hanks\\", - \\"this_update_actors0_name_SET\\": \\"Tom Hanks\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"Tom Hanks\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Tom Hanks\\" - } - } - }, - \\"edge\\": { - \\"screenTime_SET\\": { - \\"low\\": 60, - \\"high\\": 0 - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Tom Hanks\\", + \\"param2\\": \\"Tom Hanks\\", + \\"param3\\": { + \\"low\\": 60, + \\"high\\": 0 + } }" `); }); diff --git a/packages/graphql/tests/tck/deprecated/generic-filtering/projection-deprecated.test.ts b/packages/graphql/tests/tck/deprecated/generic-filtering/projection-deprecated.test.ts index 67ca842d36..dc779ba89c 100644 --- a/packages/graphql/tests/tck/deprecated/generic-filtering/projection-deprecated.test.ts +++ b/packages/graphql/tests/tck/deprecated/generic-filtering/projection-deprecated.test.ts @@ -67,12 +67,16 @@ describe("Cypher Auth Projection - deprecated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.id = $param2 WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.id = $this_update_id_SET WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -84,8 +88,7 @@ describe("Cypher Auth Projection - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"this_update_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"new-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/deprecated/generic-filtering/roles-deprecated.test.ts b/packages/graphql/tests/tck/deprecated/generic-filtering/roles-deprecated.test.ts index ab52599c0d..d17aa68452 100644 --- a/packages/graphql/tests/tck/deprecated/generic-filtering/roles-deprecated.test.ts +++ b/packages/graphql/tests/tck/deprecated/generic-filtering/roles-deprecated.test.ts @@ -348,17 +348,23 @@ describe("Cypher Auth Roles - deprecated", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - SET this.id = $this_update_id_SET + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.id = $param4 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"1\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -366,12 +372,10 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"param0\\": \\"1\\", \\"param3\\": \\"admin\\", - \\"this_update_id_SET\\": \\"id-1\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"id-1\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"admin\\" }" `); }); @@ -396,19 +400,25 @@ describe("Cypher Auth Roles - deprecated", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.password = $this_update_password_SET + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.password = $param5 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"1\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -416,13 +426,12 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"param0\\": \\"1\\", \\"param3\\": \\"admin\\", - \\"this_update_password_SET\\": \\"password\\", - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"password\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"super-admin\\", + \\"param8\\": \\"admin\\" }" `); }); @@ -447,26 +456,22 @@ describe("Cypher Auth Roles - deprecated", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - WITH this, this_posts0_connect0_node - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)-[this1:HAS_POST]->(this0) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -478,12 +483,10 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"authorization__after_param3\\": \\"super-admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"super-admin\\", + \\"param3\\": \\"super-admin\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"admin\\" }" `); }); @@ -511,36 +514,32 @@ describe("Cypher Auth Roles - deprecated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) - WITH * - CALL(*) { - WITH this, this_post0 - OPTIONAL MATCH (this_post0_creator0_connect0_node:User) - WHERE this_post0_creator0_connect0_node.id = $this_post0_creator0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH this, collect(this_post0_creator0_connect0_node) as connectedNodes, collect(this_post0) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_post0 - UNWIND connectedNodes as this_post0_creator0_connect0_node - CREATE (this_post0)-[:HAS_POST]->(this_post0_creator0_connect0_node) - } - } - WITH this, this_post0, this_post0_creator0_connect0_node - WITH this, this_post0, this_post0_creator0_connect0_node - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_post0_creator0_connect_User0 - } - RETURN count(*) AS update_this_post0 + WITH * + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_COMMENT]-(this1:Post) + WITH * + WITH * + CALL (*) { + CALL (this1) { + MATCH (this2:User) + WHERE this2.id = $param0 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this1)-[this3:HAS_POST]->(this2) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } + } } - RETURN collect(DISTINCT this { .content }) AS data" + WITH this + RETURN this { .content } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_post0_creator0_connect0_node_param0\\": \\"user-id\\", + \\"param0\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -548,11 +547,9 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"authorization__before_param2\\": \\"admin\\", - \\"authorization__before_param3\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"super-admin\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"admin\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"super-admin\\" }" `); }); @@ -576,23 +573,26 @@ describe("Cypher Auth Roles - deprecated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - WITH this, this_posts0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -604,12 +604,11 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"authorization__before_param2\\": \\"admin\\", - \\"authorization__before_param3\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"authorization__after_param3\\": \\"super-admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"super-admin\\", + \\"param3\\": \\"admin\\", + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"admin\\" }" `); }); @@ -639,32 +638,35 @@ describe("Cypher Auth Roles - deprecated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) - WITH this, this_post0 - CALL(*) { - WITH this, this_post0 - OPTIONAL MATCH (this_post0)-[this_post0_creator0_disconnect0_rel:HAS_POST]->(this_post0_creator0_disconnect0:User) - WHERE this_post0_creator0_disconnect0.id = $updateComments_args_update_post0_update_node_creator0_disconnect0_where_User_this_post0_creator0_disconnect0param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel, this_post0) { - WITH collect(this_post0_creator0_disconnect0) as this_post0_creator0_disconnect0_x, this_post0_creator0_disconnect0_rel, this_post0 - UNWIND this_post0_creator0_disconnect0_x as x - DELETE this_post0_creator0_disconnect0_rel - } - WITH this, this_post0, this_post0_creator0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_post0_creator0_disconnect_User - } - RETURN count(*) AS update_this_post0 + WITH * + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_COMMENT]-(this1:Post) + WITH * + WITH * + CALL (*) { + CALL (this1) { + OPTIONAL MATCH (this1)-[this2:HAS_POST]->(this3:User) + WHERE this3.id = $param0 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this2 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } + } } - RETURN collect(DISTINCT this { .content }) AS data" + WITH this + RETURN this { .content } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateComments_args_update_post0_update_node_creator0_disconnect0_where_User_this_post0_creator0_disconnect0param0\\": \\"user-id\\", + \\"param0\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -672,38 +674,10 @@ describe("Cypher Auth Roles - deprecated", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__after_param2\\": \\"super-admin\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"updateComments\\": { - \\"args\\": { - \\"update\\": { - \\"post\\": [ - { - \\"update\\": { - \\"node\\": { - \\"creator\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id_EQ\\": \\"user-id\\" - } - } - } - ] - } - ] - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"admin\\", + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"super-admin\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts index 88faa0cce9..4cfc4e4df5 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/allow/allow.test.ts @@ -362,15 +362,21 @@ describe("Cypher Auth Allow", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - SET this.id = $this_update_id_SET + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.id = $param3 + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"old-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -378,9 +384,7 @@ describe("Cypher Auth Allow", () => { ], \\"sub\\": \\"old-id\\" }, - \\"param0\\": \\"old-id\\", - \\"this_update_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-id\\" }" `); }); @@ -405,17 +409,22 @@ describe("Cypher Auth Allow", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.password = $param3 WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.password = $this_update_password_SET WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"id-01\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -423,9 +432,7 @@ describe("Cypher Auth Allow", () => { ], \\"sub\\": \\"id-01\\" }, - \\"param0\\": \\"id-01\\", - \\"this_update_password_SET\\": \\"new-password\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-password\\" }" `); }); @@ -452,25 +459,30 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Post) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_post0_relationship:HAS_POST]-(this_creator0:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_creator0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this_creator0.id = $this_update_creator0_id_SET - RETURN count(*) AS update_this_creator0 + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_POST]-(this1:User) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this1.id = $param3 } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_POST]-(update_this0:User) - WHERE ($jwt.sub IS NOT NULL AND update_this0.id = $jwt.sub) + MATCH (this)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"post-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -478,9 +490,7 @@ describe("Cypher Auth Allow", () => { ], \\"sub\\": \\"user-id\\" }, - \\"param0\\": \\"post-id\\", - \\"this_update_creator0_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-id\\" }" `); }); @@ -507,27 +517,31 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Post) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_post0_relationship:HAS_POST]-(this_creator0:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_creator0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH this, this_creator0 - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_creator0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this_creator0.password = $this_update_creator0_password_SET - RETURN count(*) AS update_this_creator0 + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_POST]-(this1:User) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this1.password = $param3 } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_POST]-(update_this0:User) - WHERE ($jwt.sub IS NOT NULL AND update_this0.id = $jwt.sub) + MATCH (this)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"post-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -535,9 +549,7 @@ describe("Cypher Auth Allow", () => { ], \\"sub\\": \\"user-id\\" }, - \\"param0\\": \\"post-id\\", - \\"this_update_creator0_password_SET\\": \\"new-password\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-password\\" }" `); }); @@ -656,60 +668,40 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE this_posts0_disconnect0.id = $updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + WHERE this1.id = $param1 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + } } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"user-id\\", + \\"param1\\": \\"post-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ \\"admin\\" ], \\"sub\\": \\"user-id\\" - }, - \\"param0\\": \\"user-id\\", - \\"updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0\\": \\"post-id\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"posts\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"post-id\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -740,49 +732,51 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_COMMENT]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_post0_disconnect0)<-[:HAS_POST]-(authorization__before_this1:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this1.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_post0_disconnect0, this_post0_disconnect0_rel, this) { - WITH collect(this_post0_disconnect0) as this_post0_disconnect0_x, this_post0_disconnect0_rel, this - UNWIND this_post0_disconnect0_x as x - DELETE this_post0_disconnect0_rel - } - CALL(*) { - WITH this, this_post0_disconnect0 - OPTIONAL MATCH (this_post0_disconnect0)<-[this_post0_disconnect0_creator0_rel:HAS_POST]-(this_post0_disconnect0_creator0:User) - WHERE this_post0_disconnect0_creator0.id = $updateComments_args_update_post0_disconnect0_disconnect_creator0_where_User_this_post0_disconnect0_creator0param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_post0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_post0_disconnect0_creator0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel, this_post0_disconnect0) { - WITH collect(this_post0_disconnect0_creator0) as this_post0_disconnect0_creator0_x, this_post0_disconnect0_creator0_rel, this_post0_disconnect0 - UNWIND this_post0_disconnect0_creator0_x as x - DELETE this_post0_disconnect0_creator0_rel - } - RETURN count(*) AS disconnect_this_post0_disconnect0_creator_User - } - RETURN count(*) AS disconnect_this_post0_disconnect_Post + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:HAS_COMMENT]-(this1:Post) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this)<-[:HAS_COMMENT]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)<-[this4:HAS_POST]-(this5:User) + WHERE this5.id = $param3 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this5.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this6:User) + WHERE ($jwt.sub IS NOT NULL AND this6.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this4 + } + } + WITH * + DELETE this0 + } } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_COMMENT]-(update_this0:User) - WHERE ($jwt.sub IS NOT NULL AND update_this0.id = $jwt.sub) + MATCH (this)<-[:HAS_COMMENT]-(this7:User) + WHERE ($jwt.sub IS NOT NULL AND this7.id = $jwt.sub) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"comment-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -790,36 +784,7 @@ describe("Cypher Auth Allow", () => { ], \\"sub\\": \\"user-id\\" }, - \\"param0\\": \\"comment-id\\", - \\"updateComments_args_update_post0_disconnect0_disconnect_creator0_where_User_this_post0_disconnect0_creator0param0\\": \\"user-id\\", - \\"updateComments\\": { - \\"args\\": { - \\"update\\": { - \\"post\\": [ - { - \\"disconnect\\": [ - { - \\"disconnect\\": { - \\"creator\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"user-id\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"user-id\\" }" `); }); @@ -846,43 +811,37 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - RETURN count(*) AS connect_this_posts0_connect_Post0 + CALL (*) { + CALL (this) { + MATCH (this0:Post) + WHERE this0.id = $param1 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)-[this2:HAS_POST]->(this0) + } } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"user-id\\", + \\"param1\\": \\"post-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ \\"admin\\" ], \\"sub\\": \\"user-id\\" - }, - \\"param0\\": \\"user-id\\", - \\"this_posts0_connect0_node_param0\\": \\"post-id\\", - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/allow/interface-relationships/implementation-allow.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/allow/interface-relationships/implementation-allow.test.ts index 47e6cf9188..67a9d8d6b1 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/allow/interface-relationships/implementation-allow.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/allow/interface-relationships/implementation-allow.test.ts @@ -229,57 +229,55 @@ describe("@auth allow on specific interface implementation", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Comment) - SET this_content0.id = $this_update_content0_id_SET - RETURN count(*) AS update_this_content0 - } - RETURN count(*) AS update_this_Comment - } - CALL (this){ - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Post) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_content0)<-[:HAS_CONTENT]-(authorization_updatebefore_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization_updatebefore_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this_content0.id = $this_update_content0_id_SET - RETURN count(*) AS update_this_content0 - } - RETURN count(*) AS update_this_Post + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_CONTENT]->(this1:Comment) + WITH * + SET + this1.id = $param1 } WITH * + CALL (*) { + MATCH (this)-[this2:HAS_CONTENT]->(this3:Post) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this3)<-[:HAS_CONTENT]-(this4:User) + WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this3.id = $param4 + } + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)-[update_this0:HAS_CONTENT]->(update_this1:Comment) - WITH update_this1 { .id, __resolveType: \\"Comment\\", __id: id(update_this1) } AS update_var2 - RETURN update_var2 + MATCH (this)-[this5:HAS_CONTENT]->(this6:Comment) + WITH this6 { .id, __resolveType: \\"Comment\\", __id: id(this6) } AS var7 + RETURN var7 UNION WITH * - MATCH (this)-[update_this3:HAS_CONTENT]->(update_this4:Post) + MATCH (this)-[this8:HAS_CONTENT]->(this9:Post) CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (update_this4)<-[:HAS_CONTENT]-(update_this5:User) - WHERE ($jwt.sub IS NOT NULL AND update_this5.id = $jwt.sub) + MATCH (this9)<-[:HAS_CONTENT]-(this10:User) + WHERE ($jwt.sub IS NOT NULL AND this10.id = $jwt.sub) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH update_this4 { .id, __resolveType: \\"Post\\", __id: id(update_this4) } AS update_var2 - RETURN update_var2 + WITH this9 { .id, __resolveType: \\"Post\\", __id: id(this9) } AS var7 + RETURN var7 } - WITH update_var2 - RETURN collect(update_var2) AS update_var2 + WITH var7 + RETURN collect(var7) AS var7 } - RETURN collect(DISTINCT this { .id, content: update_var2 }) AS data" + RETURN this { .id, content: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"user-id\\", + \\"param1\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -287,9 +285,7 @@ describe("@auth allow on specific interface implementation", () => { ], \\"sub\\": \\"user-id\\" }, - \\"param0\\": \\"user-id\\", - \\"this_update_content0_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"new-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts index 135ad11817..b16101fc29 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts @@ -162,16 +162,18 @@ describe("Cypher Auth isAuthenticated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Post) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"id-1\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"id-1\\" }" `); }); @@ -195,16 +197,18 @@ describe("Cypher Auth isAuthenticated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"id-1\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"id-1\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/is-authenticated.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/is-authenticated.test.ts index 3867351dfd..684950a2eb 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/is-authenticated.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/is-authenticated/is-authenticated.test.ts @@ -247,16 +247,18 @@ describe("Cypher Auth isAuthenticated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"id-1\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"id-1\\" }" `); }); @@ -280,16 +282,18 @@ describe("Cypher Auth isAuthenticated", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - SET this.password = $this_update_password_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.password = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_password_SET\\": \\"password\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"password\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts index 5128b34a1b..33669fdabe 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts @@ -607,13 +607,17 @@ describe("Cypher Auth Where with Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.name = $this_update_name_SET + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.name = $param4 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -625,14 +629,13 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", \\"param2\\": \\"user\\", \\"param3\\": \\"admin\\", - \\"this_update_name_SET\\": \\"Bob\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"Bob\\", + \\"param5\\": \\"user\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"user\\", + \\"param8\\": \\"admin\\" }" `); }); @@ -657,17 +660,23 @@ describe("Cypher Auth Where with Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.name = $param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - SET this.name = $this_update_name_SET + WHERE this.name = $param0 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.name = $param5 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"bob\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -675,15 +684,13 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"param0\\": \\"bob\\", \\"param3\\": \\"user\\", \\"param4\\": \\"admin\\", - \\"this_update_name_SET\\": \\"Bob\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param5\\": \\"Bob\\", + \\"param6\\": \\"user\\", + \\"param7\\": \\"admin\\", + \\"param8\\": \\"user\\", + \\"param9\\": \\"admin\\" }" `); }); @@ -710,31 +717,40 @@ describe("Cypher Auth Where with Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_post0_relationship:HAS_POST]->(this_posts0:Post) - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0)<-[:HAS_POST]-(authorization_updatebefore_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization_updatebefore_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization_updatebefore_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_updatebefore_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this_posts0.id = $this_update_posts0_id_SET - RETURN count(*) AS update_this_posts0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_POST]->(this1:Post) + WITH * + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this1.id = $param4 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } + WITH this WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL (this) { - MATCH (this)-[update_this0:HAS_POST]->(update_this1:Post) - WITH DISTINCT update_this1 + MATCH (this)-[this4:HAS_POST]->(this5:Post) + WITH DISTINCT this5 WITH * CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (update_this1)<-[:HAS_POST]-(update_this2:User) - WHERE ($jwt.sub IS NOT NULL AND update_this2.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $update_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH update_this1 { .id } AS update_this1 - RETURN collect(update_this1) AS update_var3 + MATCH (this5)<-[:HAS_POST]-(this6:User) + WHERE ($jwt.sub IS NOT NULL AND this6.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this5 { .id } AS this5 + RETURN collect(this5) AS var7 } - RETURN collect(DISTINCT this { .id, posts: update_var3 }) AS data" + RETURN this { .id, posts: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -746,14 +762,15 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"update_param4\\": \\"user\\", - \\"update_param5\\": \\"admin\\", - \\"authorization_updatebefore_param2\\": \\"user\\", - \\"authorization_updatebefore_param3\\": \\"admin\\", - \\"this_update_posts0_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"user\\", + \\"param3\\": \\"admin\\", + \\"param4\\": \\"new-id\\", + \\"param5\\": \\"user\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"user\\", + \\"param8\\": \\"admin\\", + \\"param9\\": \\"user\\", + \\"param10\\": \\"admin\\" }" `); }); @@ -878,7 +895,6 @@ describe("Cypher Auth Where with Roles", () => { WITH * CALL (this0) { MATCH (this1:Post) - WITH * CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { MATCH (this1)<-[:HAS_POST]-(this2:User) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) @@ -889,10 +905,12 @@ describe("Cypher Auth Where with Roles", () => { MATCH (this1)<-[:HAS_POST]-(this4:User) WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) } AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } WITH * CALL (*) { - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param12 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } RETURN this0 AS this } @@ -920,7 +938,9 @@ describe("Cypher Auth Where with Roles", () => { \\"param7\\": \\"user\\", \\"param8\\": \\"admin\\", \\"param9\\": \\"user\\", - \\"param10\\": \\"admin\\" + \\"param10\\": \\"admin\\", + \\"param11\\": \\"user\\", + \\"param12\\": \\"admin\\" }" `); }); @@ -962,7 +982,6 @@ describe("Cypher Auth Where with Roles", () => { CALL (this0) { MATCH (this1:Post) WHERE this1.id = $param3 - WITH * CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { MATCH (this1)<-[:HAS_POST]-(this2:User) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) @@ -973,10 +992,12 @@ describe("Cypher Auth Where with Roles", () => { MATCH (this1)<-[:HAS_POST]-(this4:User) WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) } AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } WITH * CALL (*) { - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param12 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param13 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } RETURN this0 AS this } @@ -1005,7 +1026,9 @@ describe("Cypher Auth Where with Roles", () => { \\"param8\\": \\"user\\", \\"param9\\": \\"admin\\", \\"param10\\": \\"user\\", - \\"param11\\": \\"admin\\" + \\"param11\\": \\"admin\\", + \\"param12\\": \\"user\\", + \\"param13\\": \\"admin\\" }" `); }); @@ -1030,32 +1053,28 @@ describe("Cypher Auth Where with Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__before_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - WITH this, this_posts0_connect0_node - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)-[this2:HAS_POST]->(this0) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1067,17 +1086,14 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"authorization__before_param2\\": \\"user\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__before_param4\\": \\"user\\", - \\"authorization__before_param5\\": \\"admin\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"authorization__after_param4\\": \\"user\\", - \\"authorization__after_param5\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"user\\", + \\"param3\\": \\"admin\\", + \\"param4\\": \\"user\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"user\\", + \\"param7\\": \\"admin\\", + \\"param8\\": \\"user\\", + \\"param9\\": \\"admin\\" }" `); }); @@ -1102,36 +1118,34 @@ describe("Cypher Auth Where with Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__before_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - WITH this, this_posts0_connect0_node - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + WHERE this0.id = $param0 + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)-[this2:HAS_POST]->(this0) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -1139,18 +1153,14 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"this_posts0_connect0_node_param0\\": \\"new-id\\", - \\"authorization__before_param2\\": \\"user\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__before_param4\\": \\"user\\", - \\"authorization__before_param5\\": \\"admin\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"authorization__after_param4\\": \\"user\\", - \\"authorization__after_param5\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"user\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"user\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"user\\", + \\"param8\\": \\"admin\\", + \\"param9\\": \\"user\\", + \\"param10\\": \\"admin\\" }" `); }); @@ -1174,29 +1184,32 @@ describe("Cypher Auth Where with Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__before_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - WITH this, this_posts0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1208,17 +1221,16 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"authorization__before_param2\\": \\"user\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__before_param4\\": \\"user\\", - \\"authorization__before_param5\\": \\"admin\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"authorization__after_param4\\": \\"user\\", - \\"authorization__after_param5\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"user\\", + \\"param3\\": \\"admin\\", + \\"param4\\": \\"user\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"user\\", + \\"param7\\": \\"admin\\", + \\"param8\\": \\"user\\", + \\"param9\\": \\"admin\\", + \\"param10\\": \\"user\\", + \\"param11\\": \\"admin\\" }" `); }); @@ -1242,33 +1254,38 @@ describe("Cypher Auth Where with Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE this_posts0_disconnect0.id = $updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0 AND (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__before_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - WITH this, this_posts0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - } AND ($jwt.roles IS NOT NULL AND $authorization__after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + WHERE this1.id = $param0 + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + } AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $param11 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param12 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -1276,39 +1293,16 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"update_param2\\": \\"user\\", - \\"update_param3\\": \\"admin\\", - \\"updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0\\": \\"new-id\\", - \\"authorization__before_param2\\": \\"user\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__before_param4\\": \\"user\\", - \\"authorization__before_param5\\": \\"admin\\", - \\"authorization__after_param2\\": \\"user\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"authorization__after_param4\\": \\"user\\", - \\"authorization__after_param5\\": \\"admin\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"posts\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"new-id\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"user\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"user\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"user\\", + \\"param8\\": \\"admin\\", + \\"param9\\": \\"user\\", + \\"param10\\": \\"admin\\", + \\"param11\\": \\"user\\", + \\"param12\\": \\"admin\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/roles/roles.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/roles/roles.test.ts index 7b4dd804ae..f783d50cfe 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/roles/roles.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/roles/roles.test.ts @@ -351,17 +351,23 @@ describe("Cypher Auth Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - SET this.id = $this_update_id_SET + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.id = $param4 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"1\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -369,12 +375,10 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"param0\\": \\"1\\", \\"param3\\": \\"admin\\", - \\"this_update_id_SET\\": \\"id-1\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"id-1\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"admin\\" }" `); }); @@ -399,19 +403,25 @@ describe("Cypher Auth Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE (this.id = $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.password = $this_update_password_SET + WHERE this.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.password = $param5 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"1\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -419,13 +429,12 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"param0\\": \\"1\\", \\"param3\\": \\"admin\\", - \\"this_update_password_SET\\": \\"password\\", - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"password\\", + \\"param6\\": \\"admin\\", + \\"param7\\": \\"super-admin\\", + \\"param8\\": \\"admin\\" }" `); }); @@ -450,26 +459,22 @@ describe("Cypher Auth Roles", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - WITH this, this_posts0_connect0_node - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)-[this1:HAS_POST]->(this0) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -481,12 +486,10 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"authorization__after_param3\\": \\"super-admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"super-admin\\", + \\"param3\\": \\"super-admin\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"admin\\" }" `); }); @@ -516,36 +519,32 @@ describe("Cypher Auth Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) - WITH * - CALL(*) { - WITH this, this_post0 - OPTIONAL MATCH (this_post0_creator0_connect0_node:User) - WHERE this_post0_creator0_connect0_node.id = $this_post0_creator0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH this, collect(this_post0_creator0_connect0_node) as connectedNodes, collect(this_post0) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_post0 - UNWIND connectedNodes as this_post0_creator0_connect0_node - CREATE (this_post0)-[:HAS_POST]->(this_post0_creator0_connect0_node) - } - } - WITH this, this_post0, this_post0_creator0_connect0_node - WITH this, this_post0, this_post0_creator0_connect0_node - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_post0_creator0_connect_User0 - } - RETURN count(*) AS update_this_post0 + WITH * + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_COMMENT]-(this1:Post) + WITH * + WITH * + CALL (*) { + CALL (this1) { + MATCH (this2:User) + WHERE this2.id = $param0 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this1)-[this3:HAS_POST]->(this2) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } + } } - RETURN collect(DISTINCT this { .content }) AS data" + WITH this + RETURN this { .content } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_post0_creator0_connect0_node_param0\\": \\"user-id\\", + \\"param0\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -553,11 +552,9 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"authorization__before_param2\\": \\"admin\\", - \\"authorization__before_param3\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"super-admin\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"admin\\", + \\"param4\\": \\"admin\\", + \\"param5\\": \\"super-admin\\" }" `); }); @@ -581,23 +578,26 @@ describe("Cypher Auth Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - WITH this, this_posts0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } + WITH this WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $update_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -609,12 +609,11 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"update_param2\\": \\"admin\\", - \\"authorization__before_param2\\": \\"admin\\", - \\"authorization__before_param3\\": \\"super-admin\\", - \\"authorization__after_param2\\": \\"admin\\", - \\"authorization__after_param3\\": \\"super-admin\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"super-admin\\", + \\"param3\\": \\"admin\\", + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"admin\\" }" `); }); @@ -646,32 +645,35 @@ describe("Cypher Auth Roles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Comment) - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) - WITH this, this_post0 - CALL(*) { - WITH this, this_post0 - OPTIONAL MATCH (this_post0)-[this_post0_creator0_disconnect0_rel:HAS_POST]->(this_post0_creator0_disconnect0:User) - WHERE this_post0_creator0_disconnect0.id = $updateComments_args_update_post0_update_node_creator0_disconnect0_where_User_this_post0_creator0_disconnect0param0 AND (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__before_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL (this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel, this_post0) { - WITH collect(this_post0_creator0_disconnect0) as this_post0_creator0_disconnect0_x, this_post0_creator0_disconnect0_rel, this_post0 - UNWIND this_post0_creator0_disconnect0_x as x - DELETE this_post0_creator0_disconnect0_rel - } - WITH this, this_post0, this_post0_creator0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization__after_param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_post0_creator0_disconnect_User - } - RETURN count(*) AS update_this_post0 + WITH * + WITH * + CALL (*) { + MATCH (this)<-[this0:HAS_COMMENT]-(this1:Post) + WITH * + WITH * + CALL (*) { + CALL (this1) { + OPTIONAL MATCH (this1)-[this2:HAS_POST]->(this3:User) + WHERE this3.id = $param0 + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this2 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } + } } - RETURN collect(DISTINCT this { .content }) AS data" + WITH this + RETURN this { .content } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateComments_args_update_post0_update_node_creator0_disconnect0_where_User_this_post0_creator0_disconnect0param0\\": \\"user-id\\", + \\"param0\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -679,40 +681,10 @@ describe("Cypher Auth Roles", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"authorization__before_param2\\": \\"super-admin\\", - \\"authorization__before_param3\\": \\"admin\\", - \\"authorization__after_param2\\": \\"super-admin\\", - \\"authorization__after_param3\\": \\"admin\\", - \\"updateComments\\": { - \\"args\\": { - \\"update\\": { - \\"post\\": [ - { - \\"update\\": { - \\"node\\": { - \\"creator\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"user-id\\" - } - } - } - } - ] - } - ] - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"admin\\", + \\"param4\\": \\"super-admin\\", + \\"param5\\": \\"admin\\", + \\"param6\\": \\"super-admin\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/validate/interface-relationships/implementation-bind.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/validate/interface-relationships/implementation-bind.test.ts index 97a59deb80..a09b43364a 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/validate/interface-relationships/implementation-bind.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/validate/interface-relationships/implementation-bind.test.ts @@ -286,81 +286,61 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Comment) - WHERE this_content0.id = $updateUsers_args_update_content0_where_this_content0param0 - WITH this, this_content0 - CALL(*) { - WITH this, this_content0 - MATCH (this_content0)<-[this_content0_has_content0_relationship:HAS_CONTENT]-(this_content0_creator0:User) - SET this_content0_creator0.id = $this_update_content0_creator0_id_SET - RETURN count(*) AS update_this_content0_creator0 - } - RETURN count(*) AS update_this_content0 - } - RETURN count(*) AS update_this_Comment - } - CALL (this){ - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Post) - WHERE this_content0.id = $updateUsers_args_update_content0_where_this_content0param0 - WITH this, this_content0 - CALL(*) { - WITH this, this_content0 - MATCH (this_content0)<-[this_content0_has_content0_relationship:HAS_CONTENT]-(this_content0_creator0:User) - SET this_content0_creator0.id = $this_update_content0_creator0_id_SET - RETURN count(*) AS update_this_content0_creator0 - } - RETURN count(*) AS update_this_content0 + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_CONTENT]->(this1:Comment) + WITH * + WHERE this1.id = $param1 + WITH * + CALL (*) { + MATCH (this1)<-[this2:HAS_CONTENT]-(this3:User) + WITH * + SET + this3.id = $param2 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - RETURN count(*) AS update_this_Post + WITH * + CALL (*) { + MATCH (this)-[this4:HAS_CONTENT]->(this5:Post) + WITH * + WHERE this5.id = $param5 + WITH * + CALL (*) { + MATCH (this5)<-[this6:HAS_CONTENT]-(this7:User) + WITH * + SET + this7.id = $param6 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this7.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this5)<-[:HAS_CONTENT]-(this8:User) + WHERE ($jwt.sub IS NOT NULL AND this8.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"id-01\\", - \\"updateUsers_args_update_content0_where_this_content0param0\\": \\"post-id\\", - \\"this_update_content0_creator0_id_SET\\": \\"not bound\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"content\\": [ - { - \\"update\\": { - \\"node\\": { - \\"creator\\": [ - { - \\"update\\": { - \\"node\\": { - \\"id_SET\\": \\"not bound\\" - } - } - } - ] - }, - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"post-id\\" - } - } - } - } - } - ] - } - } + \\"param1\\": \\"post-id\\", + \\"param2\\": \\"not bound\\", + \\"isAuthenticated\\": true, + \\"jwt\\": { + \\"roles\\": [ + \\"admin\\" + ], + \\"sub\\": \\"id-01\\" }, - \\"resolvedCallbacks\\": {} + \\"param5\\": \\"post-id\\", + \\"param6\\": \\"not bound\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/validate/validate.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/validate/validate.test.ts index 828b223146..499d8f732d 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/validate/validate.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/validate/validate.test.ts @@ -244,25 +244,27 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET + SET + this.id = $param1 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"id-01\\", - \\"this_update_id_SET\\": \\"not bound\\", + \\"param1\\": \\"not bound\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -296,60 +298,39 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_post0_relationship:HAS_POST]->(this_posts0:Post) - WHERE this_posts0.id = $updateUsers_args_update_posts0_where_this_posts0param0 - WITH this, this_posts0 - CALL(*) { - WITH this, this_posts0 - MATCH (this_posts0)<-[this_posts0_has_post0_relationship:HAS_POST]-(this_posts0_creator0:User) - SET this_posts0_creator0.id = $this_update_posts0_creator0_id_SET - RETURN count(*) AS update_this_posts0_creator0 - } - RETURN count(*) AS update_this_posts0 + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_POST]->(this1:Post) + WITH * + WHERE this1.id = $param1 + WITH * + CALL (*) { + MATCH (this1)<-[this2:HAS_POST]-(this3:User) + WITH * + SET + this3.id = $param2 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"id-01\\", - \\"updateUsers_args_update_posts0_where_this_posts0param0\\": \\"post-id\\", - \\"this_update_posts0_creator0_id_SET\\": \\"not bound\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"posts\\": [ - { - \\"update\\": { - \\"node\\": { - \\"creator\\": [ - { - \\"update\\": { - \\"node\\": { - \\"id_SET\\": \\"not bound\\" - } - } - } - ] - }, - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"post-id\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"post-id\\", + \\"param2\\": \\"not bound\\", + \\"isAuthenticated\\": true, + \\"jwt\\": { + \\"roles\\": [ + \\"admin\\" + ], + \\"sub\\": \\"id-01\\" + } }" `); }); @@ -376,43 +357,38 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Post) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_creator0_connect0_node:User) - WHERE this_creator0_connect0_node.id = $this_creator0_connect0_node_param0 - CALL(*) { - WITH collect(this_creator0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_creator0_connect0_node - CREATE (this)<-[:HAS_POST]-(this_creator0_connect0_node) - } - } - WITH this, this_creator0_connect0_node - WITH this, this_creator0_connect0_node - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_creator0_connect0_node.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_creator0_connect_User0 + CALL (*) { + CALL (this) { + MATCH (this0:User) + WHERE this0.id = $param1 + CREATE (this)<-[this1:HAS_POST]-(this0) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"post-id\\", - \\"this_creator0_connect0_node_param0\\": \\"user-id\\", + \\"param1\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -439,60 +415,39 @@ describe("Cypher Auth Allow", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Post) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_creator0_disconnect0_rel:HAS_POST]-(this_creator0_disconnect0:User) - WHERE this_creator0_disconnect0.id = $updatePosts_args_update_creator0_disconnect0_where_User_this_creator0_disconnect0param0 - CALL (this_creator0_disconnect0, this_creator0_disconnect0_rel, this) { - WITH collect(this_creator0_disconnect0) as this_creator0_disconnect0_x, this_creator0_disconnect0_rel, this - UNWIND this_creator0_disconnect0_x as x - DELETE this_creator0_disconnect0_rel - } - WITH this, this_creator0_disconnect0 - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_POST]-(authorization__after_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__after_this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this_creator0_disconnect0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS disconnect_this_creator0_disconnect_User + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:HAS_POST]-(this1:User) + WHERE this1.id = $param1 + WITH * + DELETE this0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"post-id\\", - \\"updatePosts_args_update_creator0_disconnect0_where_User_this_creator0_disconnect0param0\\": \\"user-id\\", + \\"param1\\": \\"user-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"updatePosts\\": { - \\"args\\": { - \\"update\\": { - \\"creator\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"user-id\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts index d2a8b25535..e6734def1b 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/interface-relationships/implementation-where.test.ts @@ -381,13 +381,15 @@ describe("Cypher Auth Where", () => { MATCH (this)<-[:HAS_CONTENT]-(this0:User) WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) }) - SET this.content = $this_update_content_SET + SET + this.content = $param2 + WITH this WITH * WHERE ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_CONTENT]-(update_this0:User) - WHERE ($jwt.sub IS NOT NULL AND update_this0.id = $jwt.sub) + MATCH (this)<-[:HAS_CONTENT]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) }) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -399,8 +401,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_update_content_SET\\": \\"Bob\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"Bob\\" }" `); }); @@ -427,17 +428,20 @@ describe("Cypher Auth Where", () => { MATCH (this)<-[:HAS_CONTENT]-(this0:User) WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) })) - SET this.content = $this_update_content_SET + SET + this.content = $param3 + WITH this WITH * WHERE ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:HAS_CONTENT]-(update_this0:User) - WHERE ($jwt.sub IS NOT NULL AND update_this0.id = $jwt.sub) + MATCH (this)<-[:HAS_CONTENT]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) }) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"bob\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -445,9 +449,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"param0\\": \\"bob\\", - \\"this_update_content_SET\\": \\"Bob\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"Bob\\" }" `); }); @@ -469,38 +471,34 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Comment) - SET this_content0.id = $this_update_content0_id_SET - RETURN count(*) AS update_this_content0 - } - RETURN count(*) AS update_this_Comment - } - CALL (this){ - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_content0_relationship:HAS_CONTENT]->(this_content0:Post) - WHERE ($isAuthenticated = true AND EXISTS { - MATCH (this_content0)<-[:HAS_CONTENT]-(authorization_updatebefore_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization_updatebefore_this0.id = $jwt.sub) - }) - SET this_content0.id = $this_update_content0_id_SET - RETURN count(*) AS update_this_content0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_CONTENT]->(this1:Comment) + WITH * + SET + this1.id = $param0 } - RETURN count(*) AS update_this_Post + WITH * + CALL (*) { + MATCH (this)-[this2:HAS_CONTENT]->(this3:Post) + WITH * + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this3)<-[:HAS_CONTENT]-(this4:User) + WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) + }) + SET + this3.id = $param3 } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -508,8 +506,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_update_content0_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-id\\" }" `); }); @@ -794,51 +791,26 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL (this) { WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_content0_connect0_node:Comment) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - CALL(*) { - WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_content0_connect0_node - CREATE (this)-[:HAS_CONTENT]->(this_content0_connect0_node) - } - } - WITH this, this_content0_connect0_node - RETURN count(*) AS connect_this_content0_connect_Comment0 - } - RETURN count(*) AS update_this_Comment - } - CALL (this){ - WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_content0_connect0_node:Post) - WHERE (($isAuthenticated = true AND EXISTS { - MATCH (this_content0_connect0_node)<-[:HAS_CONTENT]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - CALL(*) { - WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_content0_connect0_node - CREATE (this)-[:HAS_CONTENT]->(this_content0_connect0_node) - } - } - WITH this, this_content0_connect0_node - RETURN count(*) AS connect_this_content0_connect_Post0 - } - RETURN count(*) AS update_this_Post + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Comment) + CREATE (this)-[this1:HAS_CONTENT]->(this0) + } + CALL (this) { + MATCH (this2:Post) + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this2)<-[:HAS_CONTENT]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + }) + CREATE (this)-[this4:HAS_CONTENT]->(this2) + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -849,8 +821,7 @@ describe("Cypher Auth Where", () => { \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -872,55 +843,32 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL (this) { WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_content0_connect0_node:Comment) - WHERE this_content0_connect0_node.id = $this_content0_connect0_node_param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - CALL(*) { - WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_content0_connect0_node - CREATE (this)-[:HAS_CONTENT]->(this_content0_connect0_node) - } - } - WITH this, this_content0_connect0_node - RETURN count(*) AS connect_this_content0_connect_Comment0 - } - RETURN count(*) AS update_this_Comment - } - CALL (this){ - WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_content0_connect0_node:Post) - WHERE this_content0_connect0_node.id = $this_content0_connect0_node_param0 AND (($isAuthenticated = true AND EXISTS { - MATCH (this_content0_connect0_node)<-[:HAS_CONTENT]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - CALL(*) { - WITH collect(this_content0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_content0_connect0_node - CREATE (this)-[:HAS_CONTENT]->(this_content0_connect0_node) - } - } - WITH this, this_content0_connect0_node - RETURN count(*) AS connect_this_content0_connect_Post0 - } - RETURN count(*) AS update_this_Post + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Comment) + WHERE this0.id = $param0 + CREATE (this)-[this1:HAS_CONTENT]->(this0) + } + CALL (this) { + MATCH (this2:Post) + WHERE (($isAuthenticated = true AND EXISTS { + MATCH (this2)<-[:HAS_CONTENT]-(this3:User) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + }) AND this2.id = $param3) + CREATE (this)-[this4:HAS_CONTENT]->(this2) + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -928,8 +876,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_content0_connect0_node_param0\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-id\\" }" `); }); @@ -951,43 +898,28 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_content0_disconnect0_rel:HAS_CONTENT]->(this_content0_disconnect0:Comment) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - CALL (this_content0_disconnect0, this_content0_disconnect0_rel, this) { - WITH collect(this_content0_disconnect0) as this_content0_disconnect0_x, this_content0_disconnect0_rel, this - UNWIND this_content0_disconnect0_x as x - DELETE this_content0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_content0_disconnect_Comment - } - RETURN count(*) AS update_this_Comment + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_CONTENT]->(this1:Comment) + WITH * + DELETE this0 + } + CALL (this) { + OPTIONAL MATCH (this)-[this2:HAS_CONTENT]->(this3:Post) + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this3)<-[:HAS_CONTENT]-(this4:User) + WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) + }) + WITH * + DELETE this2 + } } - CALL (this){ - WITH this - CALL(*) { WITH this - OPTIONAL MATCH (this)-[this_content0_disconnect0_rel:HAS_CONTENT]->(this_content0_disconnect0:Post) - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND EXISTS { - MATCH (this_content0_disconnect0)<-[:HAS_CONTENT]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - })) - CALL (this_content0_disconnect0, this_content0_disconnect0_rel, this) { - WITH collect(this_content0_disconnect0) as this_content0_disconnect0_x, this_content0_disconnect0_rel, this - UNWIND this_content0_disconnect0_x as x - DELETE this_content0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_content0_disconnect_Post - } - RETURN count(*) AS update_this_Post - } WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -998,8 +930,7 @@ describe("Cypher Auth Where", () => { \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -1021,47 +952,34 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_content0_disconnect0_rel:HAS_CONTENT]->(this_content0_disconnect0:Comment) - WHERE this_content0_disconnect0.id = $updateUsers_args_update_content0_disconnect0_where_Comment_this_content0_disconnect0param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - CALL (this_content0_disconnect0, this_content0_disconnect0_rel, this) { - WITH collect(this_content0_disconnect0) as this_content0_disconnect0_x, this_content0_disconnect0_rel, this - UNWIND this_content0_disconnect0_x as x - DELETE this_content0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_content0_disconnect_Comment - } - RETURN count(*) AS update_this_Comment + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_CONTENT]->(this1:Comment) + WHERE this1.id = $param0 + WITH * + DELETE this0 + } + CALL (this) { + OPTIONAL MATCH (this)-[this2:HAS_CONTENT]->(this3:Post) + WHERE (($isAuthenticated = true AND EXISTS { + MATCH (this3)<-[:HAS_CONTENT]-(this4:User) + WHERE ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub) + }) AND this3.id = $param3) + WITH * + DELETE this2 + } } - CALL (this){ - WITH this - CALL(*) { WITH this - OPTIONAL MATCH (this)-[this_content0_disconnect0_rel:HAS_CONTENT]->(this_content0_disconnect0:Post) - WHERE this_content0_disconnect0.id = $updateUsers_args_update_content0_disconnect0_where_Post_this_content0_disconnect0param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND EXISTS { - MATCH (this_content0_disconnect0)<-[:HAS_CONTENT]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - })) - CALL (this_content0_disconnect0, this_content0_disconnect0_rel, this) { - WITH collect(this_content0_disconnect0) as this_content0_disconnect0_x, this_content0_disconnect0_rel, this - UNWIND this_content0_disconnect0_x as x - DELETE this_content0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_content0_disconnect_Post - } - RETURN count(*) AS update_this_Post - } WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"new-id\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -1069,30 +987,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"updateUsers_args_update_content0_disconnect0_where_Comment_this_content0_disconnect0param0\\": \\"new-id\\", - \\"updateUsers_args_update_content0_disconnect0_where_Post_this_content0_disconnect0param0\\": \\"new-id\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"content\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"new-id\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"new-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts index fe9eccf873..8c084127fd 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts @@ -559,10 +559,12 @@ describe("Cypher Auth Where", () => { MATCH (this:User) WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - SET this.name = $this_update_name_SET + SET + this.name = $param2 + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -574,8 +576,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_update_name_SET\\": \\"Bob\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"Bob\\" }" `); }); @@ -601,14 +602,17 @@ describe("Cypher Auth Where", () => { MATCH (this:User) WITH * WHERE (this.name = $param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - SET this.name = $this_update_name_SET + SET + this.name = $param3 + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ + \\"param0\\": \\"bob\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [ @@ -616,9 +620,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"param0\\": \\"bob\\", - \\"this_update_name_SET\\": \\"Bob\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"Bob\\" }" `); }); @@ -645,31 +647,33 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_post0_relationship:HAS_POST]->(this_posts0:Post) - WHERE ($isAuthenticated = true AND EXISTS { - MATCH (this_posts0)<-[:HAS_POST]-(authorization_updatebefore_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization_updatebefore_this0.id = $jwt.sub) - }) - SET this_posts0.id = $this_update_posts0_id_SET - RETURN count(*) AS update_this_posts0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_POST]->(this1:Post) + WITH * + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }) + SET + this1.id = $param2 } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) CALL (this) { - MATCH (this)-[update_this0:HAS_POST]->(update_this1:Post) - WITH DISTINCT update_this1 + MATCH (this)-[this3:HAS_POST]->(this4:Post) + WITH DISTINCT this4 WITH * WHERE ($isAuthenticated = true AND EXISTS { - MATCH (update_this1)<-[:HAS_POST]-(update_this2:User) - WHERE ($jwt.sub IS NOT NULL AND update_this2.id = $jwt.sub) + MATCH (this4)<-[:HAS_POST]-(this5:User) + WHERE ($jwt.sub IS NOT NULL AND this5.id = $jwt.sub) }) - WITH update_this1 { .id } AS update_this1 - RETURN collect(update_this1) AS update_var3 + WITH this4 { .id } AS this4 + RETURN collect(this4) AS var6 } - RETURN collect(DISTINCT this { .id, posts: update_var3 }) AS data" + RETURN this { .id, posts: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -681,8 +685,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_update_posts0_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"new-id\\" }" `); }); @@ -954,27 +957,21 @@ describe("Cypher Auth Where", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + }) + CREATE (this)-[this2:HAS_POST]->(this0) + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -985,8 +982,7 @@ describe("Cypher Auth Where", () => { \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -1011,27 +1007,21 @@ describe("Cypher Auth Where", () => { "CYPHER 5 MATCH (this:User) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_posts0_connect0_node:Post) - WHERE this_posts0_connect0_node.id = $this_posts0_connect0_node_param0 AND (($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_connect0_node)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - }) AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - CALL(*) { - WITH collect(this_posts0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_posts0_connect0_node - CREATE (this)-[:HAS_POST]->(this_posts0_connect0_node) - } - } - WITH this, this_posts0_connect0_node - RETURN count(*) AS connect_this_posts0_connect_Post0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Post) + WHERE (($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:HAS_POST]-(this1:User) + WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + }) AND this0.id = $param2) + CREATE (this)-[this2:HAS_POST]->(this0) + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1043,8 +1033,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"this_posts0_connect0_node_param0\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"new-id\\" }" `); }); @@ -1068,24 +1057,23 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - })) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }) + WITH * + DELETE this0 + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1096,8 +1084,7 @@ describe("Cypher Auth Where", () => { \\"admin\\" ], \\"sub\\": \\"id-01\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -1121,24 +1108,23 @@ describe("Cypher Auth Where", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_posts0_disconnect0_rel:HAS_POST]->(this_posts0_disconnect0:Post) - WHERE this_posts0_disconnect0.id = $updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0 AND (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) AND ($isAuthenticated = true AND EXISTS { - MATCH (this_posts0_disconnect0)<-[:HAS_POST]-(authorization__before_this0:User) - WHERE ($jwt.sub IS NOT NULL AND authorization__before_this0.id = $jwt.sub) - })) - CALL (this_posts0_disconnect0, this_posts0_disconnect0_rel, this) { - WITH collect(this_posts0_disconnect0) as this_posts0_disconnect0_x, this_posts0_disconnect0_rel, this - UNWIND this_posts0_disconnect0_x as x - DELETE this_posts0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_posts0_disconnect_Post + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_POST]->(this1:Post) + WHERE (($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_POST]-(this2:User) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }) AND this1.id = $param2) + WITH * + DELETE this0 + } } + WITH this WITH * WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1150,29 +1136,7 @@ describe("Cypher Auth Where", () => { ], \\"sub\\": \\"id-01\\" }, - \\"updateUsers_args_update_posts0_disconnect0_where_Post_this_posts0_disconnect0param0\\": \\"new-id\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"posts\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"new-id\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"new-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/authorization/projection.test.ts b/packages/graphql/tests/tck/directives/authorization/projection.test.ts index 406403a276..ee934a567a 100644 --- a/packages/graphql/tests/tck/directives/authorization/projection.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/projection.test.ts @@ -67,12 +67,16 @@ describe("Cypher Auth Projection", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.id = $param2 WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.id = $this_update_id_SET WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -84,8 +88,7 @@ describe("Cypher Auth Projection", () => { ], \\"sub\\": \\"super_admin\\" }, - \\"this_update_id_SET\\": \\"new-id\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"new-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/autogenerate.test.ts b/packages/graphql/tests/tck/directives/autogenerate.test.ts index cb10491e80..dbe4218124 100644 --- a/packages/graphql/tests/tck/directives/autogenerate.test.ts +++ b/packages/graphql/tests/tck/directives/autogenerate.test.ts @@ -92,14 +92,16 @@ describe("Cypher autogenerate directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.name = $this_update_name_SET - RETURN collect(DISTINCT this { .id, .name }) AS data" + WITH * + SET + this.name = $param0 + WITH this + RETURN this { .id, .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_name_SET\\": \\"dan\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"dan\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/interface-relationships/update/connect.test.ts b/packages/graphql/tests/tck/directives/interface-relationships/update/connect.test.ts index 6786c1b61e..56795a4e50 100644 --- a/packages/graphql/tests/tck/directives/interface-relationships/update/connect.test.ts +++ b/packages/graphql/tests/tck/directives/interface-relationships/update/connect.test.ts @@ -80,58 +80,40 @@ describe("Interface Relationships - Update connect", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actedIn0_connect0_node:Movie) - WHERE this_actedIn0_connect0_node.title STARTS WITH $this_actedIn0_connect0_node_param0 - CALL(*) { - WITH collect(this_actedIn0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actedIn0_connect0_node - CREATE (this)-[this_actedIn0_connect0_relationship:ACTED_IN]->(this_actedIn0_connect0_node) - SET this_actedIn0_connect0_relationship.screenTime = $this_actedIn0_connect0_relationship_screenTime - } - } - WITH this, this_actedIn0_connect0_node - RETURN count(*) AS connect_this_actedIn0_connect_Movie0 - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actedIn0_connect0_node:Series) - WHERE this_actedIn0_connect0_node.title STARTS WITH $this_actedIn0_connect0_node_param0 - CALL(*) { - WITH collect(this_actedIn0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actedIn0_connect0_node - CREATE (this)-[this_actedIn0_connect0_relationship:ACTED_IN]->(this_actedIn0_connect0_node) - SET this_actedIn0_connect0_relationship.screenTime = $this_actedIn0_connect0_relationship_screenTime - } - } - WITH this, this_actedIn0_connect0_node - RETURN count(*) AS connect_this_actedIn0_connect_Series0 - } - RETURN count(*) AS update_this_Series + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Movie) + WHERE this0.title STARTS WITH $param0 + CREATE (this)-[this1:ACTED_IN]->(this0) + SET + this1.screenTime = $param1 + } + CALL (this) { + MATCH (this2:Series) + WHERE this2.title STARTS WITH $param2 + CREATE (this)-[this3:ACTED_IN]->(this2) + SET + this3.screenTime = $param3 + } } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_actedIn0_connect0_node_param0\\": \\"The \\", - \\"this_actedIn0_connect0_relationship_screenTime\\": { + \\"param0\\": \\"The \\", + \\"param1\\": { \\"low\\": 90, \\"high\\": 0 }, - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"The \\", + \\"param3\\": { + \\"low\\": 90, + \\"high\\": 0 + } }" `); }); @@ -167,95 +149,64 @@ describe("Interface Relationships - Update connect", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actedIn0_connect0_node:Movie) - WHERE this_actedIn0_connect0_node.title STARTS WITH $this_actedIn0_connect0_node_param0 - CALL(*) { - WITH collect(this_actedIn0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actedIn0_connect0_node - CREATE (this)-[this_actedIn0_connect0_relationship:ACTED_IN]->(this_actedIn0_connect0_node) - SET this_actedIn0_connect0_relationship.screenTime = $this_actedIn0_connect0_relationship_screenTime - } - } - WITH this, this_actedIn0_connect0_node - CALL(*) { - WITH this, this_actedIn0_connect0_node - OPTIONAL MATCH (this_actedIn0_connect0_node_actors0_node:Actor) - WHERE this_actedIn0_connect0_node_actors0_node.name = $this_actedIn0_connect0_node_actors0_node_param0 - CALL(*) { - WITH this, collect(this_actedIn0_connect0_node_actors0_node) as connectedNodes, collect(this_actedIn0_connect0_node) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_actedIn0_connect0_node - UNWIND connectedNodes as this_actedIn0_connect0_node_actors0_node - CREATE (this_actedIn0_connect0_node)<-[this_actedIn0_connect0_node_actors0_relationship:ACTED_IN]-(this_actedIn0_connect0_node_actors0_node) - SET this_actedIn0_connect0_node_actors0_relationship.screenTime = $this_actedIn0_connect0_node_actors0_relationship_ActedIn_screenTime - } - } - WITH this, this_actedIn0_connect0_node, this_actedIn0_connect0_node_actors0_node - RETURN count(*) AS connect_this_actedIn0_connect0_node_actors_Actor0 - } - RETURN count(*) AS connect_this_actedIn0_connect_Movie0 - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actedIn0_connect0_node:Series) - WHERE this_actedIn0_connect0_node.title STARTS WITH $this_actedIn0_connect0_node_param0 - CALL(*) { - WITH collect(this_actedIn0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actedIn0_connect0_node - CREATE (this)-[this_actedIn0_connect0_relationship:ACTED_IN]->(this_actedIn0_connect0_node) - SET this_actedIn0_connect0_relationship.screenTime = $this_actedIn0_connect0_relationship_screenTime - } - } - WITH this, this_actedIn0_connect0_node - CALL(*) { - WITH this, this_actedIn0_connect0_node - OPTIONAL MATCH (this_actedIn0_connect0_node_actors0_node:Actor) - WHERE this_actedIn0_connect0_node_actors0_node.name = $this_actedIn0_connect0_node_actors0_node_param0 - CALL(*) { - WITH this, collect(this_actedIn0_connect0_node_actors0_node) as connectedNodes, collect(this_actedIn0_connect0_node) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_actedIn0_connect0_node - UNWIND connectedNodes as this_actedIn0_connect0_node_actors0_node - CREATE (this_actedIn0_connect0_node)<-[this_actedIn0_connect0_node_actors0_relationship:ACTED_IN]-(this_actedIn0_connect0_node_actors0_node) - SET this_actedIn0_connect0_node_actors0_relationship.screenTime = $this_actedIn0_connect0_node_actors0_relationship_ActedIn_screenTime - } - } - WITH this, this_actedIn0_connect0_node, this_actedIn0_connect0_node_actors0_node - RETURN count(*) AS connect_this_actedIn0_connect0_node_actors_Actor0 - } - RETURN count(*) AS connect_this_actedIn0_connect_Series0 - } - RETURN count(*) AS update_this_Series + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Movie) + WHERE this0.title STARTS WITH $param0 + CALL (this0) { + MATCH (this1:Actor) + WHERE this1.name = $param1 + CREATE (this0)<-[this2:ACTED_IN]-(this1) + SET + this2.screenTime = $param2 + } + CREATE (this)-[this3:ACTED_IN]->(this0) + SET + this3.screenTime = $param3 + } + CALL (this) { + MATCH (this4:Series) + WHERE this4.title STARTS WITH $param4 + CALL (this4) { + MATCH (this5:Actor) + WHERE this5.name = $param5 + CREATE (this4)<-[this6:ACTED_IN]-(this5) + SET + this6.screenTime = $param6 + } + CREATE (this)-[this7:ACTED_IN]->(this4) + SET + this7.screenTime = $param7 + } } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_actedIn0_connect0_node_param0\\": \\"The \\", - \\"this_actedIn0_connect0_relationship_screenTime\\": { + \\"param0\\": \\"The \\", + \\"param1\\": \\"Actor\\", + \\"param2\\": { + \\"low\\": 90, + \\"high\\": 0 + }, + \\"param3\\": { \\"low\\": 90, \\"high\\": 0 }, - \\"this_actedIn0_connect0_node_actors0_node_param0\\": \\"Actor\\", - \\"this_actedIn0_connect0_node_actors0_relationship_ActedIn_screenTime\\": { + \\"param4\\": \\"The \\", + \\"param5\\": \\"Actor\\", + \\"param6\\": { \\"low\\": 90, \\"high\\": 0 }, - \\"resolvedCallbacks\\": {} + \\"param7\\": { + \\"low\\": 90, + \\"high\\": 0 + } }" `); }); diff --git a/packages/graphql/tests/tck/directives/interface-relationships/update/create.test.ts b/packages/graphql/tests/tck/directives/interface-relationships/update/create.test.ts index 2fd7c34d93..09840f8242 100644 --- a/packages/graphql/tests/tck/directives/interface-relationships/update/create.test.ts +++ b/packages/graphql/tests/tck/directives/interface-relationships/update/create.test.ts @@ -89,76 +89,46 @@ describe("Interface Relationships - Update create", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CREATE (this_actedIn0_create0_node:Movie) - SET this_actedIn0_create0_node.title = $this_actedIn0_create0_node_title - SET this_actedIn0_create0_node.runtime = $this_actedIn0_create0_node_runtime - MERGE (this)-[this_actedIn0_create0_relationship:ACTED_IN]->(this_actedIn0_create0_node) - SET this_actedIn0_create0_relationship.screenTime = $updateActors.args.update.actedIn[0].create[0].edge.screenTime - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH this - RETURN count(*) AS update_this_Series - } WITH * + WITH * + CALL (*) { + CREATE (this0:Movie) + MERGE (this)-[this1:ACTED_IN]->(this0) + SET + this0.title = $param0, + this0.runtime = $param1, + this1.screenTime = $param2 + } + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH update_this1 { .title, .runtime, __resolveType: \\"Movie\\", __id: id(update_this1) } AS update_var2 - RETURN update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .title, .runtime, __resolveType: \\"Movie\\", __id: id(this3) } AS var4 + RETURN var4 UNION WITH * - MATCH (this)-[update_this3:ACTED_IN]->(update_this4:Series) - WITH update_this4 { .title, .episodes, __resolveType: \\"Series\\", __id: id(update_this4) } AS update_var2 - RETURN update_var2 + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .title, .episodes, __resolveType: \\"Series\\", __id: id(this6) } AS var4 + RETURN var4 } - WITH update_var2 - RETURN collect(update_var2) AS update_var2 + WITH var4 + RETURN collect(var4) AS var4 } - RETURN collect(DISTINCT this { .name, actedIn: update_var2 }) AS data" + RETURN this { .name, actedIn: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_actedIn0_create0_node_title\\": \\"Example Film\\", - \\"this_actedIn0_create0_node_runtime\\": { + \\"param0\\": \\"Example Film\\", + \\"param1\\": { \\"low\\": 90, \\"high\\": 0 }, - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"create\\": [ - { - \\"edge\\": { - \\"screenTime\\": { - \\"low\\": 90, - \\"high\\": 0 - } - }, - \\"node\\": { - \\"Movie\\": { - \\"title\\": \\"Example Film\\", - \\"runtime\\": { - \\"low\\": 90, - \\"high\\": 0 - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param2\\": { + \\"low\\": 90, + \\"high\\": 0 + } }" `); }); diff --git a/packages/graphql/tests/tck/directives/interface-relationships/update/delete.test.ts b/packages/graphql/tests/tck/directives/interface-relationships/update/delete.test.ts index 925868fd35..b8e953723e 100644 --- a/packages/graphql/tests/tck/directives/interface-relationships/update/delete.test.ts +++ b/packages/graphql/tests/tck/directives/interface-relationships/update/delete.test.ts @@ -74,81 +74,35 @@ describe("Interface Relationships - Update delete", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Movie) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Series) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Movie) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE this1.title STARTS WITH $param0 + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Series) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } - RETURN count(*) AS update_this_Series + CALL (*) { + OPTIONAL MATCH (this)-[this4:ACTED_IN]->(this5:Series) + WHERE this5.title STARTS WITH $param1 + WITH this4, collect(DISTINCT this5) AS var6 + CALL (var6) { + UNWIND var6 AS var7 + DETACH DELETE var7 + } } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0\\": \\"The \\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"startsWith\\": \\"The \\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"The \\", + \\"param1\\": \\"The \\" }" `); }); @@ -178,135 +132,57 @@ describe("Interface Relationships - Update delete", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Movie) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH * - CALL(*) { - OPTIONAL MATCH (this_actedIn0_delete0)<-[this_actedIn0_delete0_actors0_relationship:ACTED_IN]-(this_actedIn0_delete0_actors0:Actor) - WHERE this_actedIn0_delete0_actors0.name = $updateActors_args_update_actedIn0_delete0_delete_actors0_where_this_actedIn0_delete0_actors0param0 - WITH this_actedIn0_delete0_actors0_relationship, collect(DISTINCT this_actedIn0_delete0_actors0) AS this_actedIn0_delete0_actors0_to_delete - CALL(this_actedIn0_delete0_actors0_to_delete) { - UNWIND this_actedIn0_delete0_actors0_to_delete AS x - DETACH DELETE x - } - } - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } - WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Series) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 WITH * - CALL(*) { - OPTIONAL MATCH (this_actedIn0_delete0)<-[this_actedIn0_delete0_actors0_relationship:ACTED_IN]-(this_actedIn0_delete0_actors0:Actor) - WHERE this_actedIn0_delete0_actors0.name = $updateActors_args_update_actedIn0_delete0_delete_actors0_where_this_actedIn0_delete0_actors0param0 - WITH this_actedIn0_delete0_actors0_relationship, collect(DISTINCT this_actedIn0_delete0_actors0) AS this_actedIn0_delete0_actors0_to_delete - CALL(this_actedIn0_delete0_actors0_to_delete) { - UNWIND this_actedIn0_delete0_actors0_to_delete AS x - DETACH DELETE x - } - } - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Movie) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 WITH * - CALL(*) { - OPTIONAL MATCH (this_actedIn0_delete0)<-[this_actedIn0_delete0_actors0_relationship:ACTED_IN]-(this_actedIn0_delete0_actors0:Actor) - WHERE this_actedIn0_delete0_actors0.name = $updateActors_args_update_actedIn0_delete0_delete_actors0_where_this_actedIn0_delete0_actors0param0 - WITH this_actedIn0_delete0_actors0_relationship, collect(DISTINCT this_actedIn0_delete0_actors0) AS this_actedIn0_delete0_actors0_to_delete - CALL(this_actedIn0_delete0_actors0_to_delete) { - UNWIND this_actedIn0_delete0_actors0_to_delete AS x - DETACH DELETE x - } - } - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE this1.title STARTS WITH $param0 + WITH * + CALL (*) { + OPTIONAL MATCH (this1)<-[this2:ACTED_IN]-(this3:Actor) + WHERE this3.name = $param1 + WITH this2, collect(DISTINCT this3) AS var4 + CALL (var4) { + UNWIND var4 AS var5 + DETACH DELETE var5 + } + } + WITH this0, collect(DISTINCT this1) AS var6 + CALL (var6) { + UNWIND var6 AS var7 + DETACH DELETE var7 + } } WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_actedIn0_delete0_relationship:ACTED_IN]->(this_actedIn0_delete0:Series) - WHERE this_actedIn0_delete0.title STARTS WITH $updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0 - WITH * - CALL(*) { - OPTIONAL MATCH (this_actedIn0_delete0)<-[this_actedIn0_delete0_actors0_relationship:ACTED_IN]-(this_actedIn0_delete0_actors0:Actor) - WHERE this_actedIn0_delete0_actors0.name = $updateActors_args_update_actedIn0_delete0_delete_actors0_where_this_actedIn0_delete0_actors0param0 - WITH this_actedIn0_delete0_actors0_relationship, collect(DISTINCT this_actedIn0_delete0_actors0) AS this_actedIn0_delete0_actors0_to_delete - CALL(this_actedIn0_delete0_actors0_to_delete) { - UNWIND this_actedIn0_delete0_actors0_to_delete AS x - DETACH DELETE x - } - } - WITH this_actedIn0_delete0_relationship, collect(DISTINCT this_actedIn0_delete0) AS this_actedIn0_delete0_to_delete - CALL(this_actedIn0_delete0_to_delete) { - UNWIND this_actedIn0_delete0_to_delete AS x - DETACH DELETE x - } - } - RETURN count(*) AS update_this_Series + CALL (*) { + OPTIONAL MATCH (this)-[this8:ACTED_IN]->(this9:Series) + WHERE this9.title STARTS WITH $param2 + WITH * + CALL (*) { + OPTIONAL MATCH (this9)<-[this10:ACTED_IN]-(this11:Actor) + WHERE this11.name = $param3 + WITH this10, collect(DISTINCT this11) AS var12 + CALL (var12) { + UNWIND var12 AS var13 + DETACH DELETE var13 + } + } + WITH this8, collect(DISTINCT this9) AS var14 + CALL (var14) { + UNWIND var14 AS var15 + DETACH DELETE var15 + } } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_delete0_where_this_actedIn0_delete0param0\\": \\"The \\", - \\"updateActors_args_update_actedIn0_delete0_delete_actors0_where_this_actedIn0_delete0_actors0param0\\": \\"Actor\\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"startsWith\\": \\"The \\" - } - } - }, - \\"delete\\": { - \\"actors\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"The \\", + \\"param1\\": \\"Actor\\", + \\"param2\\": \\"The \\", + \\"param3\\": \\"Actor\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/interface-relationships/update/disconnect.test.ts b/packages/graphql/tests/tck/directives/interface-relationships/update/disconnect.test.ts index 53c98df23c..2ce816ed7a 100644 --- a/packages/graphql/tests/tck/directives/interface-relationships/update/disconnect.test.ts +++ b/packages/graphql/tests/tck/directives/interface-relationships/update/disconnect.test.ts @@ -76,66 +76,30 @@ describe("Interface Relationships - Update disconnect", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_actedIn0_disconnect0_rel:ACTED_IN]->(this_actedIn0_disconnect0:Movie) - WHERE this_actedIn0_disconnect0.title STARTS WITH $updateActors_args_update_actedIn0_disconnect0_where_Movie_this_actedIn0_disconnect0param0 - CALL (this_actedIn0_disconnect0, this_actedIn0_disconnect0_rel, this) { - WITH collect(this_actedIn0_disconnect0) as this_actedIn0_disconnect0_x, this_actedIn0_disconnect0_rel, this - UNWIND this_actedIn0_disconnect0_x as x - DELETE this_actedIn0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect_Movie - } - RETURN count(*) AS update_this_Movie + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE this1.title STARTS WITH $param0 + WITH * + DELETE this0 + } + CALL (this) { + OPTIONAL MATCH (this)-[this2:ACTED_IN]->(this3:Series) + WHERE this3.title STARTS WITH $param1 + WITH * + DELETE this2 + } } - CALL (this){ - WITH this - CALL(*) { WITH this - OPTIONAL MATCH (this)-[this_actedIn0_disconnect0_rel:ACTED_IN]->(this_actedIn0_disconnect0:Series) - WHERE this_actedIn0_disconnect0.title STARTS WITH $updateActors_args_update_actedIn0_disconnect0_where_Series_this_actedIn0_disconnect0param0 - CALL (this_actedIn0_disconnect0, this_actedIn0_disconnect0_rel, this) { - WITH collect(this_actedIn0_disconnect0) as this_actedIn0_disconnect0_x, this_actedIn0_disconnect0_rel, this - UNWIND this_actedIn0_disconnect0_x as x - DELETE this_actedIn0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect_Series - } - RETURN count(*) AS update_this_Series - } - RETURN collect(DISTINCT this { .name }) AS data" + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_disconnect0_where_Movie_this_actedIn0_disconnect0param0\\": \\"The \\", - \\"updateActors_args_update_actedIn0_disconnect0_where_Series_this_actedIn0_disconnect0param0\\": \\"The \\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"startsWith\\": \\"The \\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"The \\", + \\"param1\\": \\"The \\" }" `); }); @@ -165,102 +129,48 @@ describe("Interface Relationships - Update disconnect", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_actedIn0_disconnect0_rel:ACTED_IN]->(this_actedIn0_disconnect0:Movie) - WHERE this_actedIn0_disconnect0.title STARTS WITH $updateActors_args_update_actedIn0_disconnect0_where_Movie_this_actedIn0_disconnect0param0 - CALL (this_actedIn0_disconnect0, this_actedIn0_disconnect0_rel, this) { - WITH collect(this_actedIn0_disconnect0) as this_actedIn0_disconnect0_x, this_actedIn0_disconnect0_rel, this - UNWIND this_actedIn0_disconnect0_x as x - DELETE this_actedIn0_disconnect0_rel - } - CALL(*) { - WITH this, this_actedIn0_disconnect0 - OPTIONAL MATCH (this_actedIn0_disconnect0)<-[this_actedIn0_disconnect0_actors0_rel:ACTED_IN]-(this_actedIn0_disconnect0_actors0:Actor) - WHERE this_actedIn0_disconnect0_actors0.name = $updateActors_args_update_actedIn0_disconnect0_disconnect_actors0_where_Actor_this_actedIn0_disconnect0_actors0param0 - CALL (this_actedIn0_disconnect0_actors0, this_actedIn0_disconnect0_actors0_rel, this_actedIn0_disconnect0) { - WITH collect(this_actedIn0_disconnect0_actors0) as this_actedIn0_disconnect0_actors0_x, this_actedIn0_disconnect0_actors0_rel, this_actedIn0_disconnect0 - UNWIND this_actedIn0_disconnect0_actors0_x as x - DELETE this_actedIn0_disconnect0_actors0_rel - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect0_actors_Actor - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect_Movie - } - RETURN count(*) AS update_this_Movie + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE this1.title STARTS WITH $param0 + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)<-[this2:ACTED_IN]-(this3:Actor) + WHERE this3.name = $param1 + WITH * + DELETE this2 + } + } + WITH * + DELETE this0 + } + CALL (this) { + OPTIONAL MATCH (this)-[this4:ACTED_IN]->(this5:Series) + WHERE this5.title STARTS WITH $param2 + CALL (this5) { + CALL (this5) { + OPTIONAL MATCH (this5)<-[this6:ACTED_IN]-(this7:Actor) + WHERE this7.name = $param3 + WITH * + DELETE this6 + } + } + WITH * + DELETE this4 + } } - CALL (this){ - WITH this - CALL(*) { WITH this - OPTIONAL MATCH (this)-[this_actedIn0_disconnect0_rel:ACTED_IN]->(this_actedIn0_disconnect0:Series) - WHERE this_actedIn0_disconnect0.title STARTS WITH $updateActors_args_update_actedIn0_disconnect0_where_Series_this_actedIn0_disconnect0param0 - CALL (this_actedIn0_disconnect0, this_actedIn0_disconnect0_rel, this) { - WITH collect(this_actedIn0_disconnect0) as this_actedIn0_disconnect0_x, this_actedIn0_disconnect0_rel, this - UNWIND this_actedIn0_disconnect0_x as x - DELETE this_actedIn0_disconnect0_rel - } - CALL(*) { - WITH this, this_actedIn0_disconnect0 - OPTIONAL MATCH (this_actedIn0_disconnect0)<-[this_actedIn0_disconnect0_actors0_rel:ACTED_IN]-(this_actedIn0_disconnect0_actors0:Actor) - WHERE this_actedIn0_disconnect0_actors0.name = $updateActors_args_update_actedIn0_disconnect0_disconnect_actors0_where_Actor_this_actedIn0_disconnect0_actors0param0 - CALL (this_actedIn0_disconnect0_actors0, this_actedIn0_disconnect0_actors0_rel, this_actedIn0_disconnect0) { - WITH collect(this_actedIn0_disconnect0_actors0) as this_actedIn0_disconnect0_actors0_x, this_actedIn0_disconnect0_actors0_rel, this_actedIn0_disconnect0 - UNWIND this_actedIn0_disconnect0_actors0_x as x - DELETE this_actedIn0_disconnect0_actors0_rel - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect0_actors_Actor - } - RETURN count(*) AS disconnect_this_actedIn0_disconnect_Series - } - RETURN count(*) AS update_this_Series - } - RETURN collect(DISTINCT this { .name }) AS data" + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_disconnect0_where_Movie_this_actedIn0_disconnect0param0\\": \\"The \\", - \\"updateActors_args_update_actedIn0_disconnect0_disconnect_actors0_where_Actor_this_actedIn0_disconnect0_actors0param0\\": \\"Actor\\", - \\"updateActors_args_update_actedIn0_disconnect0_where_Series_this_actedIn0_disconnect0param0\\": \\"The \\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"startsWith\\": \\"The \\" - } - } - }, - \\"disconnect\\": { - \\"actors\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"The \\", + \\"param1\\": \\"Actor\\", + \\"param2\\": \\"The \\", + \\"param3\\": \\"Actor\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/interface-relationships/update/update.test.ts b/packages/graphql/tests/tck/directives/interface-relationships/update/update.test.ts index 46ad6c4030..ae44e14103 100644 --- a/packages/graphql/tests/tck/directives/interface-relationships/update/update.test.ts +++ b/packages/graphql/tests/tck/directives/interface-relationships/update/update.test.ts @@ -83,59 +83,33 @@ describe("Interface Relationships - Update update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - WHERE this_actedIn0.title = $updateActors_args_update_actedIn0_where_this_actedIn0param0 - SET this_actedIn0.title = $this_update_actedIn0_title_SET - RETURN count(*) AS update_this_actedIn0 - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Series) - WHERE this_actedIn0.title = $updateActors_args_update_actedIn0_where_this_actedIn0param0 - SET this_actedIn0.title = $this_update_actedIn0_title_SET - RETURN count(*) AS update_this_actedIn0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + WHERE this1.title = $param0 + SET + this1.title = $param1 } - RETURN count(*) AS update_this_Series + WITH * + CALL (*) { + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + WITH * + WHERE this3.title = $param2 + SET + this3.title = $param3 } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_where_this_actedIn0param0\\": \\"Old Title\\", - \\"this_update_actedIn0_title_SET\\": \\"New Title\\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"update\\": { - \\"node\\": { - \\"title_SET\\": \\"New Title\\" - }, - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"eq\\": \\"Old Title\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Old Title\\", + \\"param1\\": \\"New Title\\", + \\"param2\\": \\"Old Title\\", + \\"param3\\": \\"New Title\\" }" `); }); @@ -165,79 +139,43 @@ describe("Interface Relationships - Update update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - WHERE this_actedIn0.title = $updateActors_args_update_actedIn0_where_this_actedIn0param0 - WITH this, this_actedIn0 - CALL(*) { - WITH this, this_actedIn0 - MATCH (this_actedIn0)<-[this_actedIn0_acted_in0_relationship:ACTED_IN]-(this_actedIn0_actors0:Actor) - SET this_actedIn0_actors0.name = $this_update_actedIn0_actors0_name_SET - RETURN count(*) AS update_this_actedIn0_actors0 - } - RETURN count(*) AS update_this_actedIn0 - } - RETURN count(*) AS update_this_Movie - } - CALL (this){ - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Series) - WHERE this_actedIn0.title = $updateActors_args_update_actedIn0_where_this_actedIn0param0 - WITH this, this_actedIn0 - CALL(*) { - WITH this, this_actedIn0 - MATCH (this_actedIn0)<-[this_actedIn0_acted_in0_relationship:ACTED_IN]-(this_actedIn0_actors0:Actor) - SET this_actedIn0_actors0.name = $this_update_actedIn0_actors0_name_SET - RETURN count(*) AS update_this_actedIn0_actors0 - } - RETURN count(*) AS update_this_actedIn0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + WHERE this1.title = $param0 + WITH * + CALL (*) { + MATCH (this1)<-[this2:ACTED_IN]-(this3:Actor) + WITH * + SET + this3.name = $param1 + } } - RETURN count(*) AS update_this_Series + WITH * + CALL (*) { + MATCH (this)-[this4:ACTED_IN]->(this5:Series) + WITH * + WHERE this5.title = $param2 + WITH * + CALL (*) { + MATCH (this5)<-[this6:ACTED_IN]-(this7:Actor) + WITH * + SET + this7.name = $param3 + } } - RETURN collect(DISTINCT this { .name }) AS data" + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors_args_update_actedIn0_where_this_actedIn0param0\\": \\"Old Title\\", - \\"this_update_actedIn0_actors0_name_SET\\": \\"New Actor Name\\", - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"update\\": { - \\"node\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"New Actor Name\\" - } - } - } - ] - }, - \\"where\\": { - \\"node\\": { - \\"title\\": { - \\"eq\\": \\"Old Title\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Old Title\\", + \\"param1\\": \\"New Actor Name\\", + \\"param2\\": \\"Old Title\\", + \\"param3\\": \\"New Actor Name\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/node/node-additional-labels.test.ts b/packages/graphql/tests/tck/directives/node/node-additional-labels.test.ts index 9377454489..b702051002 100644 --- a/packages/graphql/tests/tck/directives/node/node-additional-labels.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-additional-labels.test.ts @@ -204,16 +204,18 @@ describe("Node directive with additionalLabels", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Film:Multimedia) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"2\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"2\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/node/node-label.test.ts b/packages/graphql/tests/tck/directives/node/node-label.test.ts index c71ed2a1e4..cd36c3c8bc 100644 --- a/packages/graphql/tests/tck/directives/node/node-label.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-label.test.ts @@ -252,16 +252,18 @@ describe("Label in Node directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Film) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"2\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"2\\" }" `); }); @@ -294,46 +296,25 @@ describe("Label in Node directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Film) + WITH * 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_SET - RETURN count(*) AS update_this_actors0 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Person) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2 } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"old name\\", - \\"this_update_actors0_name_SET\\": \\"new name\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"new name\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"old name\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"old name\\", + \\"param2\\": \\"new name\\" }" `); }); @@ -357,31 +338,24 @@ describe("Label in Node directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Film) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect0_node:Person) - WHERE this_actors0_connect0_node.name = $this_actors0_connect0_node_param0 - CALL(*) { - WITH collect(this_actors0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect0_node - CREATE (this)<-[:ACTED_IN]-(this_actors0_connect0_node) - } - } - WITH this, this_actors0_connect0_node - RETURN count(*) AS connect_this_actors0_connect_Actor0 + CALL (*) { + CALL (this) { + MATCH (this0:Person) + WHERE this0.name = $param1 + CREATE (this)<-[this1:ACTED_IN]-(this0) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_actors0_connect0_node_param0\\": \\"Daniel\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\" }" `); }); @@ -405,48 +379,25 @@ describe("Label in Node directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Film) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_actors0_disconnect0_rel:ACTED_IN]-(this_actors0_disconnect0:Person) - WHERE this_actors0_disconnect0.name = $updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0 - CALL (this_actors0_disconnect0, this_actors0_disconnect0_rel, this) { - WITH collect(this_actors0_disconnect0) as this_actors0_disconnect0_x, this_actors0_disconnect0_rel, this - UNWIND this_actors0_disconnect0_x as x - DELETE this_actors0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_actors0_disconnect_Actor + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Person) + WHERE this1.name = $param1 + WITH * + DELETE this0 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0\\": \\"Daniel\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Daniel\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\" }" `); }); diff --git a/packages/graphql/tests/tck/directives/plural.test.ts b/packages/graphql/tests/tck/directives/plural.test.ts index cf5d1da62e..67faf1b7b1 100644 --- a/packages/graphql/tests/tck/directives/plural.test.ts +++ b/packages/graphql/tests/tck/directives/plural.test.ts @@ -135,14 +135,16 @@ describe("Plural directive", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Tech) - SET this.name = $this_update_name_SET - RETURN collect(DISTINCT this { .name }) AS data" + WITH * + SET + this.name = $param0 + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_name_SET\\": \\"Matrix\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Matrix\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/1132.test.ts b/packages/graphql/tests/tck/issues/1132.test.ts index 594ec3d767..7529949415 100644 --- a/packages/graphql/tests/tck/issues/1132.test.ts +++ b/packages/graphql/tests/tck/issues/1132.test.ts @@ -68,33 +68,21 @@ describe("https://github.com/neo4j/graphql/issues/1132", () => { "CYPHER 5 MATCH (this:Source) WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_targets0_connect0_node:Target) - WHERE this_targets0_connect0_node.id = $this_targets0_connect0_node_param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL(*) { - WITH collect(this_targets0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_targets0_connect0_node - CREATE (this)-[:HAS_TARGET]->(this_targets0_connect0_node) - } - } - WITH this, this_targets0_connect0_node - RETURN count(*) AS connect_this_targets0_connect_Target0 + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Target) + WHERE this0.id = $param0 + CREATE (this)-[this1:HAS_TARGET]->(this0) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_targets0_connect0_node_param0\\": \\"1\\", - \\"isAuthenticated\\": true, - \\"jwt\\": { - \\"roles\\": [], - \\"sub\\": \\"1\\" - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"1\\" }" `); }); @@ -144,51 +132,30 @@ describe("https://github.com/neo4j/graphql/issues/1132", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Source) - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_targets0_disconnect0_rel:HAS_TARGET]->(this_targets0_disconnect0:Target) - WHERE this_targets0_disconnect0.id = $updateSources_args_update_targets0_disconnect0_where_Target_this_targets0_disconnect0param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL (this_targets0_disconnect0, this_targets0_disconnect0_rel, this) { - WITH collect(this_targets0_disconnect0) as this_targets0_disconnect0_x, this_targets0_disconnect0_rel, this - UNWIND this_targets0_disconnect0_x as x - DELETE this_targets0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_targets0_disconnect_Target + WITH * + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_TARGET]->(this1:Target) + WHERE this1.id = $param0 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + DELETE this0 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateSources_args_update_targets0_disconnect0_where_Target_this_targets0_disconnect0param0\\": \\"1\\", + \\"param0\\": \\"1\\", \\"isAuthenticated\\": true, \\"jwt\\": { \\"roles\\": [], \\"sub\\": \\"1\\" - }, - \\"updateSources\\": { - \\"args\\": { - \\"update\\": { - \\"targets\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"1\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/issues/2249.test.ts b/packages/graphql/tests/tck/issues/2249.test.ts index 9378272081..c5d1730dc8 100644 --- a/packages/graphql/tests/tck/issues/2249.test.ts +++ b/packages/graphql/tests/tck/issues/2249.test.ts @@ -87,78 +87,48 @@ describe("https://github.com/neo4j/graphql/issues/2249", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL (this) { - WITH this - CREATE (this_reviewers0_create0_node:Person) - SET this_reviewers0_create0_node.name = $this_reviewers0_create0_node_name - SET this_reviewers0_create0_node.reputation = $this_reviewers0_create0_node_reputation - MERGE (this)<-[this_reviewers0_create0_relationship:REVIEWED]-(this_reviewers0_create0_node) - SET this_reviewers0_create0_relationship.score = $updateMovies.args.update.reviewers[0].create[0].edge.score - RETURN count(*) AS update_this_Person - } - CALL (this){ - WITH this - RETURN count(*) AS update_this_Influencer - } WITH * + CALL (*) { + CREATE (this0:Person) + MERGE (this)<-[this1:REVIEWED]-(this0) + SET + this0.name = $param1, + this0.reputation = $param2, + this1.score = $param3 + } + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)<-[update_this0:REVIEWED]-(update_this1:Person) - WITH update_this1 { .name, .reputation, __resolveType: \\"Person\\", __id: id(update_this1) } AS update_var2 - RETURN update_var2 + MATCH (this)<-[this2:REVIEWED]-(this3:Person) + WITH this3 { .name, .reputation, __resolveType: \\"Person\\", __id: id(this3) } AS var4 + RETURN var4 UNION WITH * - MATCH (this)<-[update_this3:REVIEWED]-(update_this4:Influencer) - WITH update_this4 { __resolveType: \\"Influencer\\", __id: id(update_this4) } AS update_var2 - RETURN update_var2 + MATCH (this)<-[this5:REVIEWED]-(this6:Influencer) + WITH this6 { __resolveType: \\"Influencer\\", __id: id(this6) } AS var4 + RETURN var4 } - WITH update_var2 - RETURN collect(update_var2) AS update_var2 + WITH var4 + RETURN collect(var4) AS var4 } - RETURN collect(DISTINCT this { .title, reviewers: update_var2 }) AS data" + RETURN this { .title, reviewers: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Wick\\", - \\"this_reviewers0_create0_node_name\\": \\"Ana\\", - \\"this_reviewers0_create0_node_reputation\\": { + \\"param1\\": \\"Ana\\", + \\"param2\\": { \\"low\\": 100, \\"high\\": 0 }, - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"reviewers\\": [ - { - \\"create\\": [ - { - \\"edge\\": { - \\"score\\": { - \\"low\\": 10, - \\"high\\": 0 - } - }, - \\"node\\": { - \\"Person\\": { - \\"name\\": \\"Ana\\", - \\"reputation\\": { - \\"low\\": 100, - \\"high\\": 0 - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param3\\": { + \\"low\\": 10, + \\"high\\": 0 + } }" `); }); diff --git a/packages/graphql/tests/tck/issues/2782.test.ts b/packages/graphql/tests/tck/issues/2782.test.ts index b7933c0822..bd3a6f7c68 100644 --- a/packages/graphql/tests/tck/issues/2782.test.ts +++ b/packages/graphql/tests/tck/issues/2782.test.ts @@ -97,202 +97,84 @@ describe("https://github.com/neo4j/graphql/issues/2782", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Product) - SET this.id = $this_update_id_SET - SET this.name = $this_update_name_SET - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_colors0_disconnect0_rel:HAS_COLOR]->(this_colors0_disconnect0:Color) - WHERE this_colors0_disconnect0.name = $updateProducts_args_update_colors0_disconnect0_where_Color_this_colors0_disconnect0param0 - CALL (this_colors0_disconnect0, this_colors0_disconnect0_rel, this) { - WITH collect(this_colors0_disconnect0) as this_colors0_disconnect0_x, this_colors0_disconnect0_rel, this - UNWIND this_colors0_disconnect0_x as x - DELETE this_colors0_disconnect0_rel - } - CALL(*) { - WITH this, this_colors0_disconnect0 - OPTIONAL MATCH (this_colors0_disconnect0)<-[this_colors0_disconnect0_photos0_rel:OF_COLOR]-(this_colors0_disconnect0_photos0:Photo) - WHERE this_colors0_disconnect0_photos0.id = $updateProducts_args_update_colors0_disconnect0_disconnect_photos0_where_Photo_this_colors0_disconnect0_photos0param0 - CALL (this_colors0_disconnect0_photos0, this_colors0_disconnect0_photos0_rel, this_colors0_disconnect0) { - WITH collect(this_colors0_disconnect0_photos0) as this_colors0_disconnect0_photos0_x, this_colors0_disconnect0_photos0_rel, this_colors0_disconnect0 - UNWIND this_colors0_disconnect0_photos0_x as x - DELETE this_colors0_disconnect0_photos0_rel - } - CALL(*) { - WITH this, this_colors0_disconnect0, this_colors0_disconnect0_photos0 - OPTIONAL MATCH (this_colors0_disconnect0_photos0)-[this_colors0_disconnect0_photos0_color0_rel:OF_COLOR]->(this_colors0_disconnect0_photos0_color0:Color) - WHERE this_colors0_disconnect0_photos0_color0.id = $updateProducts_args_update_colors0_disconnect0_disconnect_photos0_disconnect_color0_where_Color_this_colors0_disconnect0_photos0_color0param0 - CALL (this_colors0_disconnect0_photos0_color0, this_colors0_disconnect0_photos0_color0_rel, this_colors0_disconnect0_photos0) { - WITH collect(this_colors0_disconnect0_photos0_color0) as this_colors0_disconnect0_photos0_color0_x, this_colors0_disconnect0_photos0_color0_rel, this_colors0_disconnect0_photos0 - UNWIND this_colors0_disconnect0_photos0_color0_x as x - DELETE this_colors0_disconnect0_photos0_color0_rel - } - RETURN count(*) AS disconnect_this_colors0_disconnect0_photos0_color_Color - } - RETURN count(*) AS disconnect_this_colors0_disconnect0_photos_Photo - } - RETURN count(*) AS disconnect_this_colors0_disconnect_Color - } - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_photos0_disconnect0_rel:HAS_PHOTO]->(this_photos0_disconnect0:Photo) - WHERE this_photos0_disconnect0.id = $updateProducts_args_update_photos0_disconnect0_where_Photo_this_photos0_disconnect0param0 - CALL (this_photos0_disconnect0, this_photos0_disconnect0_rel, this) { - WITH collect(this_photos0_disconnect0) as this_photos0_disconnect0_x, this_photos0_disconnect0_rel, this - UNWIND this_photos0_disconnect0_x as x - DELETE this_photos0_disconnect0_rel - } - CALL(*) { - WITH this, this_photos0_disconnect0 - OPTIONAL MATCH (this_photos0_disconnect0)-[this_photos0_disconnect0_color0_rel:OF_COLOR]->(this_photos0_disconnect0_color0:Color) - WHERE this_photos0_disconnect0_color0.name = $updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect0_color0param0 - CALL (this_photos0_disconnect0_color0, this_photos0_disconnect0_color0_rel, this_photos0_disconnect0) { - WITH collect(this_photos0_disconnect0_color0) as this_photos0_disconnect0_color0_x, this_photos0_disconnect0_color0_rel, this_photos0_disconnect0 - UNWIND this_photos0_disconnect0_color0_x as x - DELETE this_photos0_disconnect0_color0_rel + WITH * + SET + this.id = $param0, + this.name = $param1 + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_COLOR]->(this1:Color) + WHERE this1.name = $param2 + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)<-[this2:OF_COLOR]-(this3:Photo) + WHERE this3.id = $param3 + CALL (this3) { + CALL (this3) { + OPTIONAL MATCH (this3)-[this4:OF_COLOR]->(this5:Color) + WHERE this5.id = $param4 + WITH * + DELETE this4 + } + } + WITH * + DELETE this2 + } + } + WITH * + DELETE this0 + } } - RETURN count(*) AS disconnect_this_photos0_disconnect0_color_Color + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this6:HAS_PHOTO]->(this7:Photo) + WHERE this7.id = $param5 + CALL (this7) { + CALL (this7) { + OPTIONAL MATCH (this7)-[this8:OF_COLOR]->(this9:Color) + WHERE this9.name = $param6 + WITH * + DELETE this8 + } + } + WITH * + DELETE this6 + } } - RETURN count(*) AS disconnect_this_photos0_disconnect_Photo + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this10:HAS_PHOTO]->(this11:Photo) + WHERE this11.id = $param7 + CALL (this11) { + CALL (this11) { + OPTIONAL MATCH (this11)-[this12:OF_COLOR]->(this13:Color) + WHERE this13.name = $param8 + WITH * + DELETE this12 + } + } + WITH * + DELETE this10 + } } WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_photos0_disconnect1_rel:HAS_PHOTO]->(this_photos0_disconnect1:Photo) - WHERE this_photos0_disconnect1.id = $updateProducts_args_update_photos0_disconnect1_where_Photo_this_photos0_disconnect1param0 - CALL (this_photos0_disconnect1, this_photos0_disconnect1_rel, this) { - WITH collect(this_photos0_disconnect1) as this_photos0_disconnect1_x, this_photos0_disconnect1_rel, this - UNWIND this_photos0_disconnect1_x as x - DELETE this_photos0_disconnect1_rel - } - CALL(*) { - WITH this, this_photos0_disconnect1 - OPTIONAL MATCH (this_photos0_disconnect1)-[this_photos0_disconnect1_color0_rel:OF_COLOR]->(this_photos0_disconnect1_color0:Color) - WHERE this_photos0_disconnect1_color0.name = $updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect1_color0param0 - CALL (this_photos0_disconnect1_color0, this_photos0_disconnect1_color0_rel, this_photos0_disconnect1) { - WITH collect(this_photos0_disconnect1_color0) as this_photos0_disconnect1_color0_x, this_photos0_disconnect1_color0_rel, this_photos0_disconnect1 - UNWIND this_photos0_disconnect1_color0_x as x - DELETE this_photos0_disconnect1_color0_rel - } - RETURN count(*) AS disconnect_this_photos0_disconnect1_color_Color - } - RETURN count(*) AS disconnect_this_photos0_disconnect_Photo - } - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_id_SET\\": \\"123\\", - \\"this_update_name_SET\\": \\"Nested Connect\\", - \\"updateProducts_args_update_colors0_disconnect0_where_Color_this_colors0_disconnect0param0\\": \\"Red\\", - \\"updateProducts_args_update_colors0_disconnect0_disconnect_photos0_where_Photo_this_colors0_disconnect0_photos0param0\\": \\"123\\", - \\"updateProducts_args_update_colors0_disconnect0_disconnect_photos0_disconnect_color0_where_Color_this_colors0_disconnect0_photos0_color0param0\\": \\"134\\", - \\"updateProducts_args_update_photos0_disconnect0_where_Photo_this_photos0_disconnect0param0\\": \\"321\\", - \\"updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect0_color0param0\\": \\"Green\\", - \\"updateProducts_args_update_photos0_disconnect1_where_Photo_this_photos0_disconnect1param0\\": \\"33211\\", - \\"updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect1_color0param0\\": \\"Red\\", - \\"updateProducts\\": { - \\"args\\": { - \\"update\\": { - \\"id_SET\\": \\"123\\", - \\"name_SET\\": \\"Nested Connect\\", - \\"colors\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Red\\" - } - } - }, - \\"disconnect\\": { - \\"photos\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"123\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"134\\" - } - } - } - } - ] - } - } - ] - } - } - ] - } - ], - \\"photos\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"321\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Green\\" - } - } - } - } - ] - } - }, - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"33211\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Red\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"123\\", + \\"param1\\": \\"Nested Connect\\", + \\"param2\\": \\"Red\\", + \\"param3\\": \\"123\\", + \\"param4\\": \\"134\\", + \\"param5\\": \\"321\\", + \\"param6\\": \\"Green\\", + \\"param7\\": \\"33211\\", + \\"param8\\": \\"Red\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/2789.test.ts b/packages/graphql/tests/tck/issues/2789.test.ts index 3f18bc0b94..b45aef543d 100644 --- a/packages/graphql/tests/tck/issues/2789.test.ts +++ b/packages/graphql/tests/tck/issues/2789.test.ts @@ -53,28 +53,32 @@ describe("https://github.com/neo4j/graphql/issues/2789", () => { "CYPHER 5 MATCH (this:User) WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($param1 IS NOT NULL AND this.id = $param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($authorization__before_param1 IS NOT NULL AND this.id = $authorization__before_param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - SET this.password = $this_update_password_SET + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param1 IS NOT NULL AND this.id = $param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param2 IS NOT NULL AND this.id = $param2)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.password = $param3 + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param4 IS NOT NULL AND this.id = $param4)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param5 IS NOT NULL AND this.id = $param5)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH this - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($authorization__after_param1 IS NOT NULL AND this.id = $authorization__after_param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($authorization__after_param1 IS NOT NULL AND this.id = $authorization__after_param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) WITH * - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($update_param1 IS NOT NULL AND this.id = $update_param1)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($update_param2 IS NOT NULL AND this.id = $update_param2)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN collect(DISTINCT this { .password }) AS data" + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param6 IS NOT NULL AND this.id = $param6)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND ($param7 IS NOT NULL AND this.id = $param7)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .password } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"isAuthenticated\\": false, - \\"update_param1\\": \\"Foo\\", - \\"update_param2\\": \\"Bar\\", \\"param1\\": \\"Foo\\", - \\"this_update_password_SET\\": \\"123\\", - \\"authorization__before_param1\\": \\"Bar\\", - \\"authorization__after_param1\\": \\"Foo\\", - \\"resolvedCallbacks\\": {} + \\"param2\\": \\"Bar\\", + \\"param3\\": \\"123\\", + \\"param4\\": \\"Foo\\", + \\"param5\\": \\"Bar\\", + \\"param6\\": \\"Foo\\", + \\"param7\\": \\"Bar\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/2803.test.ts b/packages/graphql/tests/tck/issues/2803.test.ts index c76a778fab..309b207ba1 100644 --- a/packages/graphql/tests/tck/issues/2803.test.ts +++ b/packages/graphql/tests/tck/issues/2803.test.ts @@ -1336,8 +1336,10 @@ describe("https://github.com/neo4j/graphql/issues/2803", () => { } WITH * WHERE var8 = true - SET this.name = $this_update_name_SET - RETURN collect(DISTINCT this { .name }) AS data" + SET + this.name = $param3 + WITH this + RETURN this { .name } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -1348,8 +1350,7 @@ describe("https://github.com/neo4j/graphql/issues/2803", () => { }, \\"param1\\": \\"some role\\", \\"param2\\": \\"another role\\", - \\"this_update_name_SET\\": \\"Exciting new name!\\", - \\"resolvedCallbacks\\": {} + \\"param3\\": \\"Exciting new name!\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/288.test.ts b/packages/graphql/tests/tck/issues/288.test.ts index 85a7c9cc77..3843e7cd55 100644 --- a/packages/graphql/tests/tck/issues/288.test.ts +++ b/packages/graphql/tests/tck/issues/288.test.ts @@ -98,16 +98,18 @@ describe("#288", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:USER) + WITH * WHERE this.USERID = $param0 - SET this.COMPANYID = $this_update_COMPANYID_SET - RETURN collect(DISTINCT this { .USERID, .COMPANYID }) AS data" + SET + this.COMPANYID = $param1 + WITH this + RETURN this { .USERID, .COMPANYID } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"userid\\", - \\"this_update_COMPANYID_SET\\": \\"companyid2\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"companyid2\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/4214.test.ts b/packages/graphql/tests/tck/issues/4214.test.ts index 82aa03fb79..86ce9ab499 100644 --- a/packages/graphql/tests/tck/issues/4214.test.ts +++ b/packages/graphql/tests/tck/issues/4214.test.ts @@ -184,7 +184,6 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { MATCH (this1)-[:TRANSACTION]->(this2:Store) WHERE ($jwt.store IS NOT NULL AND this2.id = $jwt.store) })) AND this1.id = $param8) - WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $param10 IN $jwt.roles)) AND EXISTS { MATCH (this1)-[:TRANSACTION]->(this3:Store) WHERE ($jwt.store IS NOT NULL AND this3.id = $jwt.store) @@ -195,9 +194,7 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { MATCH (this1)-[:TRANSACTION]->(this5:Store) WHERE ($jwt.store IS NOT NULL AND this5.id = $jwt.store) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - } - WITH * - CALL (*) { + WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $param13 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $param14 IN $jwt.roles)) AND EXISTS { MATCH (this0)-[:ITEM_TRANSACTED]->(this6:Transaction) WHERE EXISTS { @@ -206,29 +203,39 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { } }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } + WITH * + CALL (*) { + CALL apoc.util.validate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $param15 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $param16 IN $jwt.roles)) AND EXISTS { + MATCH (this0)-[:ITEM_TRANSACTED]->(this8:Transaction) + WHERE EXISTS { + MATCH (this8)-[:TRANSACTION]->(this9:Store) + WHERE ($jwt.store IS NOT NULL AND this9.id = $jwt.store) + } + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } RETURN this0 AS this } WITH this CALL (this) { CALL (this) { - MATCH (this)-[this8:ITEM_TRANSACTED]->(this9:Transaction) - WHERE (($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param15 IN $jwt.roles)) OR ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $param16 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $param17 IN $jwt.roles)) AND EXISTS { - MATCH (this9)-[:TRANSACTION]->(this10:Store) - WHERE ($jwt.store IS NOT NULL AND this10.id = $jwt.store) + MATCH (this)-[this10:ITEM_TRANSACTED]->(this11:Transaction) + WHERE (($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param17 IN $jwt.roles)) OR ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $param18 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $param19 IN $jwt.roles)) AND EXISTS { + MATCH (this11)-[:TRANSACTION]->(this12:Store) + WHERE ($jwt.store IS NOT NULL AND this12.id = $jwt.store) })) - WITH DISTINCT this9 - CALL (this9) { - MATCH (this9)-[this11:TRANSACTION]->(this12:Store) - WITH DISTINCT this12 - WITH this12 { .name } AS this12 - RETURN collect(this12) AS var13 + WITH DISTINCT this11 + CALL (this11) { + MATCH (this11)-[this13:TRANSACTION]->(this14:Store) + WITH DISTINCT this14 + WITH this14 { .name } AS this14 + RETURN collect(this14) AS var15 } - WITH this9 { .id, store: var13 } AS this9 - RETURN collect(this9) AS var14 + WITH this11 { .id, store: var15 } AS this11 + RETURN collect(this11) AS var16 } - RETURN this { .name, transaction: var14 } AS var15 + RETURN this { .name, transaction: var16 } AS var17 } - RETURN collect(var15) AS data" + RETURN collect(var17) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -256,9 +263,11 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { \\"param12\\": \\"employee\\", \\"param13\\": \\"store-owner\\", \\"param14\\": \\"employee\\", - \\"param15\\": \\"admin\\", - \\"param16\\": \\"store-owner\\", - \\"param17\\": \\"employee\\" + \\"param15\\": \\"store-owner\\", + \\"param16\\": \\"employee\\", + \\"param17\\": \\"admin\\", + \\"param18\\": \\"store-owner\\", + \\"param19\\": \\"employee\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/5023.test.ts b/packages/graphql/tests/tck/issues/5023.test.ts index 48170bb70f..9c9b8382dc 100644 --- a/packages/graphql/tests/tck/issues/5023.test.ts +++ b/packages/graphql/tests/tck/issues/5023.test.ts @@ -141,95 +141,106 @@ describe("https://github.com/neo4j/graphql/issues/5023", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Tenant) - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_settings0_relationship:HAS_SETTINGS]->(this_settings0:Settings) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_settings0)<-[:HAS_SETTINGS]-(authorization_updatebefore_this0:Tenant) - WHERE EXISTS { - MATCH (authorization_updatebefore_this0)<-[:ADMIN_IN]-(authorization_updatebefore_this1:User) - WHERE ($jwt.id IS NOT NULL AND authorization_updatebefore_this1.userId = $jwt.id) - } - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH * - CALL(*) { - OPTIONAL MATCH (this_settings0)-[this_settings0_extendedOpeningHours0_delete0_relationship:HAS_OPENING_HOURS]->(this_settings0_extendedOpeningHours0_delete0:OpeningDay) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this_settings0_extendedOpeningHours0_delete0)<-[:HAS_OPENING_HOURS]-(authorization_deletebefore_this0:Settings) - WHERE EXISTS { - MATCH (authorization_deletebefore_this0)<-[:HAS_SETTINGS]-(authorization_deletebefore_this1:Tenant) - WHERE EXISTS { - MATCH (authorization_deletebefore_this1)<-[:ADMIN_IN]-(authorization_deletebefore_this2:User) - WHERE ($jwt.id IS NOT NULL AND authorization_deletebefore_this2.userId = $jwt.id) - } - } - }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH this_settings0_extendedOpeningHours0_delete0_relationship, collect(DISTINCT this_settings0_extendedOpeningHours0_delete0) AS this_settings0_extendedOpeningHours0_delete0_to_delete - CALL(this_settings0_extendedOpeningHours0_delete0_to_delete) { - UNWIND this_settings0_extendedOpeningHours0_delete0_to_delete AS x - DETACH DELETE x - } - } - RETURN count(*) AS update_this_settings0 + WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_SETTINGS]->(this1:Settings) + WITH * + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_SETTINGS]-(this2:Tenant) + WHERE EXISTS { + MATCH (this2)<-[:ADMIN_IN]-(this3:User) + WHERE ($jwt.id IS NOT NULL AND this3.userId = $jwt.id) + } + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + WITH * + CALL (*) { + OPTIONAL MATCH (this1)-[this4:HAS_OPENING_HOURS]->(this5:OpeningDay) + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this5)<-[:HAS_OPENING_HOURS]-(this6:Settings) + WHERE EXISTS { + MATCH (this6)<-[:HAS_SETTINGS]-(this7:Tenant) + WHERE EXISTS { + MATCH (this7)<-[:ADMIN_IN]-(this8:User) + WHERE ($jwt.id IS NOT NULL AND this8.userId = $jwt.id) + } + } + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this4, collect(DISTINCT this5) AS var9 + CALL (var9) { + UNWIND var9 AS var10 + DETACH DELETE var10 + } + } + WITH * + CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this1)<-[:HAS_SETTINGS]-(this11:Tenant) + WHERE EXISTS { + MATCH (this11)<-[:ADMIN_IN]-(this12:User) + WHERE ($jwt.id IS NOT NULL AND this12.userId = $jwt.id) + } + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) } + WITH this WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:ADMIN_IN]-(update_this0:User) - WHERE ($jwt.id IS NOT NULL AND update_this0.userId = $jwt.id) + MATCH (this)<-[:ADMIN_IN]-(this13:User) + WHERE ($jwt.id IS NOT NULL AND this13.userId = $jwt.id) }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL (this) { - MATCH (this)-[update_this1:HAS_SETTINGS]->(update_this2:Settings) - WITH DISTINCT update_this2 + MATCH (this)-[this14:HAS_SETTINGS]->(this15:Settings) + WITH DISTINCT this15 WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (update_this2)<-[:HAS_SETTINGS]-(update_this3:Tenant) + MATCH (this15)<-[:HAS_SETTINGS]-(this16:Tenant) WHERE EXISTS { - MATCH (update_this3)<-[:ADMIN_IN]-(update_this4:User) - WHERE ($jwt.id IS NOT NULL AND update_this4.userId = $jwt.id) + MATCH (this16)<-[:ADMIN_IN]-(this17:User) + WHERE ($jwt.id IS NOT NULL AND this17.userId = $jwt.id) } }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL (update_this2) { - MATCH (update_this2)-[update_this5:HAS_OPENING_HOURS]->(update_this6:OpeningDay) - WITH DISTINCT update_this6 + CALL (this15) { + MATCH (this15)-[this18:HAS_OPENING_HOURS]->(this19:OpeningDay) + WITH DISTINCT this19 WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (update_this6)<-[:HAS_OPENING_HOURS]-(update_this7:Settings) + MATCH (this19)<-[:HAS_OPENING_HOURS]-(this20:Settings) WHERE EXISTS { - MATCH (update_this7)<-[:HAS_SETTINGS]-(update_this8:Tenant) + MATCH (this20)<-[:HAS_SETTINGS]-(this21:Tenant) WHERE EXISTS { - MATCH (update_this8)<-[:ADMIN_IN]-(update_this9:User) - WHERE ($jwt.id IS NOT NULL AND update_this9.userId = $jwt.id) + MATCH (this21)<-[:ADMIN_IN]-(this22:User) + WHERE ($jwt.id IS NOT NULL AND this22.userId = $jwt.id) } } }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL (update_this6) { - MATCH (update_this6)-[update_this10:HAS_OPEN_INTERVALS]->(update_this11:OpeningHoursInterval) - WITH DISTINCT update_this11 + CALL (this19) { + MATCH (this19)-[this23:HAS_OPEN_INTERVALS]->(this24:OpeningHoursInterval) + WITH DISTINCT this24 WITH * CALL apoc.util.validate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (update_this11)<-[:HAS_OPEN_INTERVALS]-(update_this12:OpeningDay) + MATCH (this24)<-[:HAS_OPEN_INTERVALS]-(this25:OpeningDay) WHERE EXISTS { - MATCH (update_this12)<-[:HAS_OPENING_HOURS]-(update_this13:Settings) + MATCH (this25)<-[:HAS_OPENING_HOURS]-(this26:Settings) WHERE EXISTS { - MATCH (update_this13)<-[:HAS_SETTINGS]-(update_this14:Tenant) + MATCH (this26)<-[:HAS_SETTINGS]-(this27:Tenant) WHERE EXISTS { - MATCH (update_this14)<-[:ADMIN_IN]-(update_this15:User) - WHERE ($jwt.id IS NOT NULL AND update_this15.userId = $jwt.id) + MATCH (this27)<-[:ADMIN_IN]-(this28:User) + WHERE ($jwt.id IS NOT NULL AND this28.userId = $jwt.id) } } } }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH update_this11 { .name } AS update_this11 - RETURN collect(update_this11) AS update_var16 + WITH this24 { .name } AS this24 + RETURN collect(this24) AS var29 } - WITH update_this6 { open: update_var16 } AS update_this6 - RETURN collect(update_this6) AS update_var17 + WITH this19 { open: var29 } AS this19 + RETURN collect(this19) AS var30 } - WITH update_this2 { extendedOpeningHours: update_var17 } AS update_this2 - RETURN collect(update_this2) AS update_var18 + WITH this15 { extendedOpeningHours: var30 } AS this15 + RETURN collect(this15) AS var31 } - RETURN collect(DISTINCT this { settings: update_var18 }) AS data" + RETURN this { settings: var31 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -237,8 +248,7 @@ describe("https://github.com/neo4j/graphql/issues/5023", () => { \\"isAuthenticated\\": true, \\"jwt\\": { \\"id\\": \\"myUserId\\" - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/issues/5599.test.ts b/packages/graphql/tests/tck/issues/5599.test.ts index 467163dd1f..e2b75eeba3 100644 --- a/packages/graphql/tests/tck/issues/5599.test.ts +++ b/packages/graphql/tests/tck/issues/5599.test.ts @@ -68,45 +68,23 @@ describe("https://github.com/neo4j/graphql/issues/5599", () => { "CYPHER 5 MATCH (this:Movie) WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors_LeadActor0_delete0_relationship:ACTED_IN]-(this_actors_LeadActor0_delete0:LeadActor) - WHERE this_actors_LeadActor0_delete0.name = $updateMovies_args_update_actors0_delete_LeadActor0_where_this_actors_LeadActor0_delete0param0 - WITH this_actors_LeadActor0_delete0_relationship, collect(DISTINCT this_actors_LeadActor0_delete0) AS this_actors_LeadActor0_delete0_to_delete - CALL(this_actors_LeadActor0_delete0_to_delete) { - UNWIND this_actors_LeadActor0_delete0_to_delete AS x - DETACH DELETE x - } + WITH * + CALL (*) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:LeadActor) + WHERE this1.name = $param0 + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateMovies_args_update_actors0_delete_LeadActor0_where_this_actors_LeadActor0_delete0param0\\": \\"Actor1\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": { - \\"LeadActor\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor1\\" - } - } - } - } - ] - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Actor1\\" }" `); }); @@ -135,71 +113,34 @@ describe("https://github.com/neo4j/graphql/issues/5599", () => { "CYPHER 5 MATCH (this:Movie) WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors_LeadActor0_delete0_relationship:ACTED_IN]-(this_actors_LeadActor0_delete0:LeadActor) - WHERE this_actors_LeadActor0_delete0.name = $updateMovies_args_update_actors0_delete_LeadActor0_where_this_actors_LeadActor0_delete0param0 - WITH this_actors_LeadActor0_delete0_relationship, collect(DISTINCT this_actors_LeadActor0_delete0) AS this_actors_LeadActor0_delete0_to_delete - CALL(this_actors_LeadActor0_delete0_to_delete) { - UNWIND this_actors_LeadActor0_delete0_to_delete AS x - DETACH DELETE x - } - } WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors_Extra0_delete0_relationship:ACTED_IN]-(this_actors_Extra0_delete0:Extra) - WHERE this_actors_Extra0_delete0.name = $updateMovies_args_update_actors0_delete_Extra0_where_this_actors_Extra0_delete0param0 - WITH this_actors_Extra0_delete0_relationship, collect(DISTINCT this_actors_Extra0_delete0) AS this_actors_Extra0_delete0_to_delete - CALL(this_actors_Extra0_delete0_to_delete) { - UNWIND this_actors_Extra0_delete0_to_delete AS x - DETACH DELETE x + CALL (*) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:LeadActor) + WHERE this1.name = $param0 + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } + WITH * + CALL (*) { + OPTIONAL MATCH (this)<-[this4:ACTED_IN]-(this5:Extra) + WHERE this5.name = $param1 + WITH this4, collect(DISTINCT this5) AS var6 + CALL (var6) { + UNWIND var6 AS var7 + DETACH DELETE var7 + } } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateMovies_args_update_actors0_delete_LeadActor0_where_this_actors_LeadActor0_delete0param0\\": \\"Actor1\\", - \\"updateMovies_args_update_actors0_delete_Extra0_where_this_actors_Extra0_delete0param0\\": \\"Actor2\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": { - \\"LeadActor\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor1\\" - } - } - } - } - ] - } - ], - \\"Extra\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor2\\" - } - } - } - } - ] - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"Actor1\\", + \\"param1\\": \\"Actor2\\" }" `); }); diff --git a/packages/graphql/tests/tck/issues/6620.test.ts b/packages/graphql/tests/tck/issues/6620.test.ts index bec5e23b5f..4c3d2a1b1b 100644 --- a/packages/graphql/tests/tck/issues/6620.test.ts +++ b/packages/graphql/tests/tck/issues/6620.test.ts @@ -205,24 +205,17 @@ describe("https://github.com/neo4j/graphql/issues/6620", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Car) + WITH * WHERE this.name = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_producedBy0_connect0_node:CarManufacturer) - WHERE this_producedBy0_connect0_node.name = $this_producedBy0_connect0_node_param0 AND ((this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)) AND (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP))) - CALL(*) { - WITH collect(this_producedBy0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_producedBy0_connect0_node - CREATE (this)<-[:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this_producedBy0_connect0_node) - } - } - WITH this, this_producedBy0_connect0_node - RETURN count(*) AS connect_this_producedBy0_connect_CarManufacturer0 + CALL (*) { + CALL (this) { + MATCH (this0:CarManufacturer) + WHERE ((this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)) AND this0.name = $param2) + CREATE (this)<-[this1:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this0) + } } - RETURN \\"Query cannot conclude with CALL\\"" + FINISH" `); }); @@ -250,24 +243,17 @@ describe("https://github.com/neo4j/graphql/issues/6620", () => { MATCH (this:Car) WITH * WHERE (this.name = $param0 AND (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_UPDATE IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_UPDATE))) - SET this.name = $this_update_name.set + SET + this.name = $param2 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_producedBy0_connect0_node:CarManufacturer) - WHERE this_producedBy0_connect0_node.name = $this_producedBy0_connect0_node_param0 AND ((this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)) AND (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP))) - CALL(*) { - WITH collect(this_producedBy0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_producedBy0_connect0_node - CREATE (this)<-[:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this_producedBy0_connect0_node) - } - } - WITH this, this_producedBy0_connect0_node - RETURN count(*) AS connect_this_producedBy0_connect_CarManufacturer0 + CALL (*) { + CALL (this) { + MATCH (this0:CarManufacturer) + WHERE ((this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)) AND this0.name = $param3) + CREATE (this)<-[this1:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this0) + } } - RETURN \\"Query cannot conclude with CALL\\"" + FINISH" `); }); }); @@ -457,26 +443,22 @@ describe("https://github.com/neo4j/graphql/issues/6620 validate", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Car) + WITH * WHERE this.name = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_producedBy0_connect0_node:CarManufacturer) - WHERE this_producedBy0_connect0_node.name = $this_producedBy0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_producedBy0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_producedBy0_connect0_node - CREATE (this)<-[:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this_producedBy0_connect0_node) - } - } - WITH this, this_producedBy0_connect0_node - WITH this, this_producedBy0_connect0_node - WHERE (apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_producedBy0_connect_CarManufacturer0 + CALL (*) { + CALL (this) { + MATCH (this0:CarManufacturer) + WHERE this0.name = $param1 + CALL apoc.util.validate(NOT (this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)<-[this1:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this0) + WITH * + CALL apoc.util.validate(NOT (this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - RETURN \\"Query cannot conclude with CALL\\"" + FINISH" `); }); @@ -503,29 +485,28 @@ describe("https://github.com/neo4j/graphql/issues/6620 validate", () => { "CYPHER 5 MATCH (this:Car) WITH * - WHERE (this.name = $param0 AND apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_UPDATE IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_UPDATE)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - SET this.name = $this_update_name.set + WHERE this.name = $param0 + WITH * + CALL apoc.util.validate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_UPDATE IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_UPDATE)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + SET + this.name = $param2 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_producedBy0_connect0_node:CarManufacturer) - WHERE this_producedBy0_connect0_node.name = $this_producedBy0_connect0_node_param0 AND (apoc.util.validatePredicate(NOT (this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL(*) { - WITH collect(this_producedBy0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_producedBy0_connect0_node - CREATE (this)<-[:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this_producedBy0_connect0_node) - } - } - WITH this, this_producedBy0_connect0_node - WITH this, this_producedBy0_connect0_node - WHERE (apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (this_producedBy0_connect0_node.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this_producedBy0_connect0_node.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN count(*) AS connect_this_producedBy0_connect_CarManufacturer0 + CALL (*) { + CALL (this) { + MATCH (this0:CarManufacturer) + WHERE this0.name = $param3 + CALL apoc.util.validate(NOT (this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CREATE (this)<-[this1:CAR_IS_PRODUCED_BY_CARMANUFACTURER]-(this0) + WITH * + CALL apoc.util.validate(NOT (this0.accessibleBy IS NULL OR ($jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP IS NOT NULL AND this0.accessibleBy IN $jwt.permission_CarManufacturer_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH * + CALL apoc.util.validate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_CREATE_RELATIONSHIP IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_CREATE_RELATIONSHIP)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + } } - WITH this - WHERE apoc.util.validatePredicate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_UPDATE IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_UPDATE)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - RETURN \\"Query cannot conclude with CALL\\"" + WITH * + CALL apoc.util.validate(NOT (this.accessibleBy IS NULL OR ($jwt.permission_Car_node_UPDATE IS NOT NULL AND this.accessibleBy IN $jwt.permission_Car_node_UPDATE)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + FINISH" `); }); }); diff --git a/packages/graphql/tests/tck/issues/894.test.ts b/packages/graphql/tests/tck/issues/894.test.ts index 85179a6afb..4b947ed2c3 100644 --- a/packages/graphql/tests/tck/issues/894.test.ts +++ b/packages/graphql/tests/tck/issues/894.test.ts @@ -67,78 +67,34 @@ describe("https://github.com/neo4j/graphql/issues/894", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE this.name = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_activeOrganization0_disconnect0_rel:ACTIVELY_MANAGING]->(this_activeOrganization0_disconnect0:Organization) - WHERE NOT (this_activeOrganization0_disconnect0._id = $updateUsers_args_update_activeOrganization0_disconnect0_where_Organization_this_activeOrganization0_disconnect0param0) - CALL (this_activeOrganization0_disconnect0, this_activeOrganization0_disconnect0_rel, this) { - WITH collect(this_activeOrganization0_disconnect0) as this_activeOrganization0_disconnect0_x, this_activeOrganization0_disconnect0_rel, this - UNWIND this_activeOrganization0_disconnect0_x as x - DELETE this_activeOrganization0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_activeOrganization0_disconnect_Organization + WITH * + CALL (*) { + CALL (this) { + MATCH (this0:Organization) + WHERE this0._id = $param1 + CREATE (this)-[this1:ACTIVELY_MANAGING]->(this0) + } } WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_activeOrganization0_connect0_node:Organization) - WHERE this_activeOrganization0_connect0_node._id = $this_activeOrganization0_connect0_node_param0 - CALL(*) { - WITH collect(this_activeOrganization0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_activeOrganization0_connect0_node - CREATE (this)-[:ACTIVELY_MANAGING]->(this_activeOrganization0_connect0_node) - } - } - WITH this, this_activeOrganization0_connect0_node - RETURN count(*) AS connect_this_activeOrganization0_connect_Organization0 + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this2:ACTIVELY_MANAGING]->(this3:Organization) + WHERE NOT (this3._id = $param2) + WITH * + DELETE this2 + } } - RETURN collect(DISTINCT this { id: this._id }) AS data" + WITH this + RETURN this { id: this._id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Luke Skywalker\\", - \\"updateUsers_args_update_activeOrganization0_disconnect0_where_Organization_this_activeOrganization0_disconnect0param0\\": \\"test-id\\", - \\"this_activeOrganization0_connect0_node_param0\\": \\"test-id\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"activeOrganization\\": [ - { - \\"connect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"test-id\\" - } - } - } - } - ], - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"NOT\\": { - \\"id\\": { - \\"eq\\": \\"test-id\\" - } - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"test-id\\", + \\"param2\\": \\"test-id\\" }" `); }); diff --git a/packages/graphql/tests/tck/math.test.ts b/packages/graphql/tests/tck/math.test.ts index 6ef82892ed..f6435a8385 100644 --- a/packages/graphql/tests/tck/math.test.ts +++ b/packages/graphql/tests/tck/math.test.ts @@ -76,23 +76,24 @@ describe("Math operators", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - WITH this - CALL(this) { - WITH this - WHERE apoc.util.validatePredicate(this.viewers IS NULL, 'Cannot %s %s to Nan', [\\"_INCREMENT\\", $this_update_viewers_INCREMENT]) AND apoc.util.validatePredicate(this.viewers IS NOT NULL AND this.viewers + $this_update_viewers_INCREMENT > 2^31-1, 'Overflow: Value returned from operator %s is larger than %s bit', [\\"_INCREMENT\\", \\"32\\"]) - SET this.viewers = this.viewers + $this_update_viewers_INCREMENT - RETURN this as this_viewers__INCREMENT + WITH * + SET + this.viewers = (this.viewers + $param0) + WITH * + CALL (*) { + CALL apoc.util.validate(this.viewers IS NULL, \\"Cannot %s %s to Nan\\", [\\"increment\\", $param0]) + CALL apoc.util.validate((this.viewers + $param0) > ((2 ^ 31) - 1), \\"Overflow: Value returned from operator %s is larger than %s bit\\", [\\"increment\\", 32]) } - RETURN collect(DISTINCT this { .id, .viewers }) AS data" + WITH this + RETURN this { .id, .viewers } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_viewers_INCREMENT\\": { + \\"param0\\": { \\"low\\": 3, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -113,20 +114,21 @@ describe("Math operators", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - WITH this - CALL(this) { - WITH this - WHERE apoc.util.validatePredicate(this.revenue IS NULL, 'Cannot %s %s to Nan', [\\"_MULTIPLY\\", $this_update_revenue_MULTIPLY]) AND apoc.util.validatePredicate(this.revenue IS NOT NULL AND this.revenue * $this_update_revenue_MULTIPLY > 2^63-1, 'Overflow: Value returned from operator %s is larger than %s bit', [\\"_MULTIPLY\\", \\"64\\"]) - SET this.revenue = this.revenue * $this_update_revenue_MULTIPLY - RETURN this as this_revenue__MULTIPLY + WITH * + SET + this.revenue = (this.revenue * $param0) + WITH * + CALL (*) { + CALL apoc.util.validate(this.revenue IS NULL, \\"Cannot %s %s to Nan\\", [\\"multiply\\", $param0]) + CALL apoc.util.validate((this.revenue * $param0) > ((2 ^ 63) - 1), \\"Overflow: Value returned from operator %s is larger than %s bit\\", [\\"multiply\\", 64]) } - RETURN collect(DISTINCT this { .id, .revenue }) AS data" + WITH this + RETURN this { .id, .revenue } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_revenue_MULTIPLY\\": 3, - \\"resolvedCallbacks\\": {} + \\"param0\\": 3 }" `); }); @@ -149,36 +151,35 @@ describe("Math operators", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - WITH this_actedIn0, this - CALL(this_actedIn0) { - WITH this_actedIn0 - WHERE apoc.util.validatePredicate(this_actedIn0.viewers IS NULL, 'Cannot %s %s to Nan', [\\"_INCREMENT\\", $this_update_actedIn0_viewers_INCREMENT]) AND apoc.util.validatePredicate(this_actedIn0.viewers IS NOT NULL AND this_actedIn0.viewers + $this_update_actedIn0_viewers_INCREMENT > 2^31-1, 'Overflow: Value returned from operator %s is larger than %s bit', [\\"_INCREMENT\\", \\"32\\"]) - SET this_actedIn0.viewers = this_actedIn0.viewers + $this_update_actedIn0_viewers_INCREMENT - RETURN this_actedIn0 as this_actedIn0_viewers__INCREMENT - } - RETURN count(*) AS update_this_actedIn0 - } WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + SET + this1.viewers = (this1.viewers + $param0) + WITH * + CALL (*) { + CALL apoc.util.validate(this1.viewers IS NULL, \\"Cannot %s %s to Nan\\", [\\"increment\\", $param0]) + CALL apoc.util.validate((this1.viewers + $param0) > ((2 ^ 31) - 1), \\"Overflow: Value returned from operator %s is larger than %s bit\\", [\\"increment\\", 32]) + } + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .viewers } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .viewers } AS this3 + RETURN collect(this3) AS var4 } - RETURN collect(DISTINCT this { .name, actedIn: update_var2 }) AS data" + RETURN this { .name, actedIn: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_actedIn0_viewers_INCREMENT\\": { + \\"param0\\": { \\"low\\": 10, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -208,57 +209,42 @@ describe("Math operators", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_acted_in0_relationship:ACTED_IN]->(this_actedIn0:Movie) - WITH this_acted_in0_relationship, this - CALL(this_acted_in0_relationship) { - WITH this_acted_in0_relationship - WHERE apoc.util.validatePredicate(this_acted_in0_relationship.pay IS NULL, 'Cannot %s %s to Nan', [\\"_ADD\\", $updateActors.args.update.actedIn[0].update.edge.pay_ADD]) AND apoc.util.validatePredicate(this_acted_in0_relationship.pay IS NOT NULL AND this_acted_in0_relationship.pay + $updateActors.args.update.actedIn[0].update.edge.pay_ADD > 2^63-1, 'Overflow: Value returned from operator %s is larger than %s bit', [\\"_ADD\\", \\"64\\"]) - SET this_acted_in0_relationship.pay = this_acted_in0_relationship.pay + $updateActors.args.update.actedIn[0].update.edge.pay_ADD - RETURN this_acted_in0_relationship as this_acted_in0_relationship_pay__ADD - } - RETURN count(*) AS update_this_actedIn0 - } WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH * + SET + this0.pay = (this0.pay + $param0) + WITH * + CALL (*) { + CALL apoc.util.validate(this0.pay IS NULL, \\"Cannot %s %s to Nan\\", [\\"add\\", $param0]) + CALL apoc.util.validate((this0.pay + $param0) > ((2 ^ 63) - 1), \\"Overflow: Value returned from operator %s is larger than %s bit\\", [\\"add\\", 64]) + } + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .title } AS this3 + RETURN collect(this3) AS var4 } CALL (this) { - MATCH (this)-[update_this3:ACTED_IN]->(update_this4:Movie) - WITH collect({ node: update_this4, relationship: update_this3 }) AS edges + MATCH (this)-[this5:ACTED_IN]->(this6:Movie) + WITH collect({ node: this6, relationship: this5 }) AS edges CALL (edges) { UNWIND edges AS edge - WITH edge.node AS update_this4, edge.relationship AS update_this3 - RETURN collect({ properties: { pay: update_this3.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(update_this4), __resolveType: \\"Movie\\" } }) AS update_var5 + WITH edge.node AS this6, edge.relationship AS this5 + RETURN collect({ properties: { pay: this5.pay, __resolveType: \\"ActedIn\\" }, node: { __id: id(this6), __resolveType: \\"Movie\\" } }) AS var7 } - RETURN { edges: update_var5 } AS update_var6 + RETURN { edges: var7 } AS var8 } - RETURN collect(DISTINCT this { .name, actedIn: update_var2, actedInConnection: update_var6 }) AS data" + RETURN this { .name, actedIn: var4, actedInConnection: var8 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"updateActors\\": { - \\"args\\": { - \\"update\\": { - \\"actedIn\\": [ - { - \\"update\\": { - \\"edge\\": { - \\"pay_ADD\\": 100 - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": 100 }" `); }); @@ -281,44 +267,39 @@ describe("Math operators", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) - WITH this - CALL (this) { - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_married_with0_relationship:MARRIED_WITH]->(this_marriedWith0:Star) - WITH this_marriedWith0, this - CALL(this_marriedWith0) { - WITH this_marriedWith0 - WHERE apoc.util.validatePredicate(this_marriedWith0.marriageLength IS NULL, 'Cannot %s %s to Nan', [\\"_INCREMENT\\", $this_update_marriedWith0_marriageLength_INCREMENT]) AND apoc.util.validatePredicate(this_marriedWith0.marriageLength IS NOT NULL AND this_marriedWith0.marriageLength + $this_update_marriedWith0_marriageLength_INCREMENT > 2^31-1, 'Overflow: Value returned from operator %s is larger than %s bit', [\\"_INCREMENT\\", \\"32\\"]) - SET this_marriedWith0.marriageLength = this_marriedWith0.marriageLength + $this_update_marriedWith0_marriageLength_INCREMENT - RETURN this_marriedWith0 as this_marriedWith0_marriageLength__INCREMENT - } - RETURN count(*) AS update_this_marriedWith0 - } - RETURN count(*) AS update_this_Star - } WITH * + WITH * + CALL (*) { + MATCH (this)-[this0:MARRIED_WITH]->(this1:Star) + WITH * + SET + this1.marriageLength = (this1.marriageLength + $param0) + WITH * + CALL (*) { + CALL apoc.util.validate(this1.marriageLength IS NULL, \\"Cannot %s %s to Nan\\", [\\"increment\\", $param0]) + CALL apoc.util.validate((this1.marriageLength + $param0) > ((2 ^ 31) - 1), \\"Overflow: Value returned from operator %s is larger than %s bit\\", [\\"increment\\", 32]) + } + } + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)-[update_this0:MARRIED_WITH]->(update_this1:Star) - WITH update_this1 { .marriageLength, __resolveType: \\"Star\\", __id: id(update_this1) } AS update_var2 - RETURN update_var2 + MATCH (this)-[this2:MARRIED_WITH]->(this3:Star) + WITH this3 { .marriageLength, __resolveType: \\"Star\\", __id: id(this3) } AS var4 + RETURN var4 } - WITH update_var2 - RETURN collect(update_var2) AS update_var2 + WITH var4 + RETURN collect(var4) AS var4 } - RETURN collect(DISTINCT this { .name, marriedWith: update_var2 }) AS data" + RETURN this { .name, marriedWith: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_marriedWith0_marriageLength_INCREMENT\\": { + \\"param0\\": { \\"low\\": 1, \\"high\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/nested-unions.test.ts b/packages/graphql/tests/tck/nested-unions.test.ts index 8d12295972..9662460db9 100644 --- a/packages/graphql/tests/tck/nested-unions.test.ts +++ b/packages/graphql/tests/tck/nested-unions.test.ts @@ -94,78 +94,62 @@ describe("Nested Unions", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors_LeadActor0_connect0_node:LeadActor) - WHERE this_actors_LeadActor0_connect0_node.name = $this_actors_LeadActor0_connect0_node_param0 - CALL(*) { - WITH collect(this_actors_LeadActor0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors_LeadActor0_connect0_node - CREATE (this)<-[:ACTED_IN]-(this_actors_LeadActor0_connect0_node) - } - } - WITH this, this_actors_LeadActor0_connect0_node - CALL(*) { - WITH this, this_actors_LeadActor0_connect0_node - OPTIONAL MATCH (this_actors_LeadActor0_connect0_node_actedIn_Series0_node:Series) - WHERE this_actors_LeadActor0_connect0_node_actedIn_Series0_node.name = $this_actors_LeadActor0_connect0_node_actedIn_Series0_node_param0 - CALL(*) { - WITH this, collect(this_actors_LeadActor0_connect0_node_actedIn_Series0_node) as connectedNodes, collect(this_actors_LeadActor0_connect0_node) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_actors_LeadActor0_connect0_node - UNWIND connectedNodes as this_actors_LeadActor0_connect0_node_actedIn_Series0_node - CREATE (this_actors_LeadActor0_connect0_node)-[:ACTED_IN]->(this_actors_LeadActor0_connect0_node_actedIn_Series0_node) - } - } - WITH this, this_actors_LeadActor0_connect0_node, this_actors_LeadActor0_connect0_node_actedIn_Series0_node - RETURN count(*) AS connect_this_actors_LeadActor0_connect0_node_actedIn_Series_Series0 - } - RETURN count(*) AS connect_this_actors_LeadActor0_connect_LeadActor0 + CALL (*) { + CALL (this) { + MATCH (this0:LeadActor) + WHERE this0.name = $param1 + CALL (this0) { + MATCH (this1:Movie) + CREATE (this0)-[this2:ACTED_IN]->(this1) + } + CALL (this0) { + MATCH (this3:Series) + CREATE (this0)-[this4:ACTED_IN]->(this3) + } + CREATE (this)<-[this5:ACTED_IN]-(this0) + } } - WITH * + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)<-[update_this0:ACTED_IN]-(update_this1:LeadActor) - CALL (update_this1) { + MATCH (this)<-[this6:ACTED_IN]-(this7:LeadActor) + CALL (this7) { CALL (*) { WITH * - MATCH (update_this1)-[update_this2:ACTED_IN]->(update_this3:Movie) - WITH update_this3 { __resolveType: \\"Movie\\", __id: id(update_this3) } AS update_var4 - RETURN update_var4 + MATCH (this7)-[this8:ACTED_IN]->(this9:Movie) + WITH this9 { __resolveType: \\"Movie\\", __id: id(this9) } AS var10 + RETURN var10 UNION WITH * - MATCH (update_this1)-[update_this5:ACTED_IN]->(update_this6:Series) - WITH update_this6 { .name, __resolveType: \\"Series\\", __id: id(update_this6) } AS update_var4 - RETURN update_var4 + MATCH (this7)-[this11:ACTED_IN]->(this12:Series) + WITH this12 { .name, __resolveType: \\"Series\\", __id: id(this12) } AS var10 + RETURN var10 } - WITH update_var4 - RETURN collect(update_var4) AS update_var4 + WITH var10 + RETURN collect(var10) AS var10 } - WITH update_this1 { .name, actedIn: update_var4, __resolveType: \\"LeadActor\\", __id: id(update_this1) } AS update_var7 - RETURN update_var7 + WITH this7 { .name, actedIn: var10, __resolveType: \\"LeadActor\\", __id: id(this7) } AS var13 + RETURN var13 UNION WITH * - MATCH (this)<-[update_this8:ACTED_IN]-(update_this9:Extra) - WITH update_this9 { __resolveType: \\"Extra\\", __id: id(update_this9) } AS update_var7 - RETURN update_var7 + MATCH (this)<-[this14:ACTED_IN]-(this15:Extra) + WITH this15 { __resolveType: \\"Extra\\", __id: id(this15) } AS var13 + RETURN var13 } - WITH update_var7 - RETURN collect(update_var7) AS update_var7 + WITH var13 + RETURN collect(var13) AS var13 } - RETURN collect(DISTINCT this { .title, actors: update_var7 }) AS data" + RETURN this { .title, actors: var13 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Movie\\", - \\"this_actors_LeadActor0_connect0_node_param0\\": \\"Actor\\", - \\"this_actors_LeadActor0_connect0_node_actedIn_Series0_node_param0\\": \\"Series\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Actor\\" }" `); }); @@ -208,108 +192,69 @@ describe("Nested Unions", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_actors_LeadActor0_disconnect0_rel:ACTED_IN]-(this_actors_LeadActor0_disconnect0:LeadActor) - WHERE this_actors_LeadActor0_disconnect0.name = $updateMovies_args_update_actors_LeadActor0_disconnect0_where_LeadActor_this_actors_LeadActor0_disconnect0param0 - CALL (this_actors_LeadActor0_disconnect0, this_actors_LeadActor0_disconnect0_rel, this) { - WITH collect(this_actors_LeadActor0_disconnect0) as this_actors_LeadActor0_disconnect0_x, this_actors_LeadActor0_disconnect0_rel, this - UNWIND this_actors_LeadActor0_disconnect0_x as x - DELETE this_actors_LeadActor0_disconnect0_rel - } - CALL(*) { - WITH this, this_actors_LeadActor0_disconnect0 - OPTIONAL MATCH (this_actors_LeadActor0_disconnect0)-[this_actors_LeadActor0_disconnect0_actedIn_Series0_rel:ACTED_IN]->(this_actors_LeadActor0_disconnect0_actedIn_Series0:Series) - WHERE this_actors_LeadActor0_disconnect0_actedIn_Series0.name = $updateMovies_args_update_actors_LeadActor0_disconnect0_disconnect_actedIn_Series0_where_Series_this_actors_LeadActor0_disconnect0_actedIn_Series0param0 - CALL (this_actors_LeadActor0_disconnect0_actedIn_Series0, this_actors_LeadActor0_disconnect0_actedIn_Series0_rel, this_actors_LeadActor0_disconnect0) { - WITH collect(this_actors_LeadActor0_disconnect0_actedIn_Series0) as this_actors_LeadActor0_disconnect0_actedIn_Series0_x, this_actors_LeadActor0_disconnect0_actedIn_Series0_rel, this_actors_LeadActor0_disconnect0 - UNWIND this_actors_LeadActor0_disconnect0_actedIn_Series0_x as x - DELETE this_actors_LeadActor0_disconnect0_actedIn_Series0_rel - } - RETURN count(*) AS disconnect_this_actors_LeadActor0_disconnect0_actedIn_Series_Series - } - RETURN count(*) AS disconnect_this_actors_LeadActor0_disconnect_LeadActor - } WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:LeadActor) + WHERE this1.name = $param1 + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WITH * + DELETE this2 + } + } + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)-[this4:ACTED_IN]->(this5:Series) + WITH * + DELETE this4 + } + } + WITH * + DELETE this0 + } + } + WITH this CALL (this) { CALL (*) { WITH * - MATCH (this)<-[update_this0:ACTED_IN]-(update_this1:LeadActor) - CALL (update_this1) { + MATCH (this)<-[this6:ACTED_IN]-(this7:LeadActor) + CALL (this7) { CALL (*) { WITH * - MATCH (update_this1)-[update_this2:ACTED_IN]->(update_this3:Movie) - WITH update_this3 { __resolveType: \\"Movie\\", __id: id(update_this3) } AS update_var4 - RETURN update_var4 + MATCH (this7)-[this8:ACTED_IN]->(this9:Movie) + WITH this9 { __resolveType: \\"Movie\\", __id: id(this9) } AS var10 + RETURN var10 UNION WITH * - MATCH (update_this1)-[update_this5:ACTED_IN]->(update_this6:Series) - WITH update_this6 { .name, __resolveType: \\"Series\\", __id: id(update_this6) } AS update_var4 - RETURN update_var4 + MATCH (this7)-[this11:ACTED_IN]->(this12:Series) + WITH this12 { .name, __resolveType: \\"Series\\", __id: id(this12) } AS var10 + RETURN var10 } - WITH update_var4 - RETURN collect(update_var4) AS update_var4 + WITH var10 + RETURN collect(var10) AS var10 } - WITH update_this1 { .name, actedIn: update_var4, __resolveType: \\"LeadActor\\", __id: id(update_this1) } AS update_var7 - RETURN update_var7 + WITH this7 { .name, actedIn: var10, __resolveType: \\"LeadActor\\", __id: id(this7) } AS var13 + RETURN var13 UNION WITH * - MATCH (this)<-[update_this8:ACTED_IN]-(update_this9:Extra) - WITH update_this9 { __resolveType: \\"Extra\\", __id: id(update_this9) } AS update_var7 - RETURN update_var7 + MATCH (this)<-[this14:ACTED_IN]-(this15:Extra) + WITH this15 { __resolveType: \\"Extra\\", __id: id(this15) } AS var13 + RETURN var13 } - WITH update_var7 - RETURN collect(update_var7) AS update_var7 + WITH var13 + RETURN collect(var13) AS var13 } - RETURN collect(DISTINCT this { .title, actors: update_var7 }) AS data" + RETURN this { .title, actors: var13 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Movie\\", - \\"updateMovies_args_update_actors_LeadActor0_disconnect0_where_LeadActor_this_actors_LeadActor0_disconnect0param0\\": \\"Actor\\", - \\"updateMovies_args_update_actors_LeadActor0_disconnect0_disconnect_actedIn_Series0_where_Series_this_actors_LeadActor0_disconnect0_actedIn_Series0param0\\": \\"Series\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": { - \\"LeadActor\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor\\" - } - } - }, - \\"disconnect\\": { - \\"actedIn\\": { - \\"Series\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Series\\" - } - } - } - } - ] - } - } - } - ] - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Actor\\" }" `); }); diff --git a/packages/graphql/tests/tck/operations/disconnect.test.ts b/packages/graphql/tests/tck/operations/disconnect.test.ts index 7d879d2718..70b0964bf8 100644 --- a/packages/graphql/tests/tck/operations/disconnect.test.ts +++ b/packages/graphql/tests/tck/operations/disconnect.test.ts @@ -106,202 +106,84 @@ describe("Cypher Disconnect", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Product) - SET this.id = $this_update_id_SET - SET this.name = $this_update_name_SET - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_colors0_disconnect0_rel:HAS_COLOR]->(this_colors0_disconnect0:Color) - WHERE this_colors0_disconnect0.name = $updateProducts_args_update_colors0_disconnect0_where_Color_this_colors0_disconnect0param0 - CALL (this_colors0_disconnect0, this_colors0_disconnect0_rel, this) { - WITH collect(this_colors0_disconnect0) as this_colors0_disconnect0_x, this_colors0_disconnect0_rel, this - UNWIND this_colors0_disconnect0_x as x - DELETE this_colors0_disconnect0_rel - } - CALL(*) { - WITH this, this_colors0_disconnect0 - OPTIONAL MATCH (this_colors0_disconnect0)<-[this_colors0_disconnect0_photos0_rel:OF_COLOR]-(this_colors0_disconnect0_photos0:Photo) - WHERE this_colors0_disconnect0_photos0.id = $updateProducts_args_update_colors0_disconnect0_disconnect_photos0_where_Photo_this_colors0_disconnect0_photos0param0 - CALL (this_colors0_disconnect0_photos0, this_colors0_disconnect0_photos0_rel, this_colors0_disconnect0) { - WITH collect(this_colors0_disconnect0_photos0) as this_colors0_disconnect0_photos0_x, this_colors0_disconnect0_photos0_rel, this_colors0_disconnect0 - UNWIND this_colors0_disconnect0_photos0_x as x - DELETE this_colors0_disconnect0_photos0_rel - } - CALL(*) { - WITH this, this_colors0_disconnect0, this_colors0_disconnect0_photos0 - OPTIONAL MATCH (this_colors0_disconnect0_photos0)-[this_colors0_disconnect0_photos0_color0_rel:OF_COLOR]->(this_colors0_disconnect0_photos0_color0:Color) - WHERE this_colors0_disconnect0_photos0_color0.id = $updateProducts_args_update_colors0_disconnect0_disconnect_photos0_disconnect_color0_where_Color_this_colors0_disconnect0_photos0_color0param0 - CALL (this_colors0_disconnect0_photos0_color0, this_colors0_disconnect0_photos0_color0_rel, this_colors0_disconnect0_photos0) { - WITH collect(this_colors0_disconnect0_photos0_color0) as this_colors0_disconnect0_photos0_color0_x, this_colors0_disconnect0_photos0_color0_rel, this_colors0_disconnect0_photos0 - UNWIND this_colors0_disconnect0_photos0_color0_x as x - DELETE this_colors0_disconnect0_photos0_color0_rel - } - RETURN count(*) AS disconnect_this_colors0_disconnect0_photos0_color_Color - } - RETURN count(*) AS disconnect_this_colors0_disconnect0_photos_Photo - } - RETURN count(*) AS disconnect_this_colors0_disconnect_Color - } - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_photos0_disconnect0_rel:HAS_PHOTO]->(this_photos0_disconnect0:Photo) - WHERE this_photos0_disconnect0.id = $updateProducts_args_update_photos0_disconnect0_where_Photo_this_photos0_disconnect0param0 - CALL (this_photos0_disconnect0, this_photos0_disconnect0_rel, this) { - WITH collect(this_photos0_disconnect0) as this_photos0_disconnect0_x, this_photos0_disconnect0_rel, this - UNWIND this_photos0_disconnect0_x as x - DELETE this_photos0_disconnect0_rel - } - CALL(*) { - WITH this, this_photos0_disconnect0 - OPTIONAL MATCH (this_photos0_disconnect0)-[this_photos0_disconnect0_color0_rel:OF_COLOR]->(this_photos0_disconnect0_color0:Color) - WHERE this_photos0_disconnect0_color0.name = $updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect0_color0param0 - CALL (this_photos0_disconnect0_color0, this_photos0_disconnect0_color0_rel, this_photos0_disconnect0) { - WITH collect(this_photos0_disconnect0_color0) as this_photos0_disconnect0_color0_x, this_photos0_disconnect0_color0_rel, this_photos0_disconnect0 - UNWIND this_photos0_disconnect0_color0_x as x - DELETE this_photos0_disconnect0_color0_rel + WITH * + SET + this.id = $param0, + this.name = $param1 + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:HAS_COLOR]->(this1:Color) + WHERE this1.name = $param2 + CALL (this1) { + CALL (this1) { + OPTIONAL MATCH (this1)<-[this2:OF_COLOR]-(this3:Photo) + WHERE this3.id = $param3 + CALL (this3) { + CALL (this3) { + OPTIONAL MATCH (this3)-[this4:OF_COLOR]->(this5:Color) + WHERE this5.id = $param4 + WITH * + DELETE this4 + } + } + WITH * + DELETE this2 + } + } + WITH * + DELETE this0 + } } - RETURN count(*) AS disconnect_this_photos0_disconnect0_color_Color + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this6:HAS_PHOTO]->(this7:Photo) + WHERE this7.id = $param5 + CALL (this7) { + CALL (this7) { + OPTIONAL MATCH (this7)-[this8:OF_COLOR]->(this9:Color) + WHERE this9.name = $param6 + WITH * + DELETE this8 + } + } + WITH * + DELETE this6 + } } - RETURN count(*) AS disconnect_this_photos0_disconnect_Photo + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this10:HAS_PHOTO]->(this11:Photo) + WHERE this11.id = $param7 + CALL (this11) { + CALL (this11) { + OPTIONAL MATCH (this11)-[this12:OF_COLOR]->(this13:Color) + WHERE this13.name = $param8 + WITH * + DELETE this12 + } + } + WITH * + DELETE this10 + } } WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_photos0_disconnect1_rel:HAS_PHOTO]->(this_photos0_disconnect1:Photo) - WHERE this_photos0_disconnect1.id = $updateProducts_args_update_photos0_disconnect1_where_Photo_this_photos0_disconnect1param0 - CALL (this_photos0_disconnect1, this_photos0_disconnect1_rel, this) { - WITH collect(this_photos0_disconnect1) as this_photos0_disconnect1_x, this_photos0_disconnect1_rel, this - UNWIND this_photos0_disconnect1_x as x - DELETE this_photos0_disconnect1_rel - } - CALL(*) { - WITH this, this_photos0_disconnect1 - OPTIONAL MATCH (this_photos0_disconnect1)-[this_photos0_disconnect1_color0_rel:OF_COLOR]->(this_photos0_disconnect1_color0:Color) - WHERE this_photos0_disconnect1_color0.name = $updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect1_color0param0 - CALL (this_photos0_disconnect1_color0, this_photos0_disconnect1_color0_rel, this_photos0_disconnect1) { - WITH collect(this_photos0_disconnect1_color0) as this_photos0_disconnect1_color0_x, this_photos0_disconnect1_color0_rel, this_photos0_disconnect1 - UNWIND this_photos0_disconnect1_color0_x as x - DELETE this_photos0_disconnect1_color0_rel - } - RETURN count(*) AS disconnect_this_photos0_disconnect1_color_Color - } - RETURN count(*) AS disconnect_this_photos0_disconnect_Photo - } - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_id_SET\\": \\"123\\", - \\"this_update_name_SET\\": \\"Nested Connect\\", - \\"updateProducts_args_update_colors0_disconnect0_where_Color_this_colors0_disconnect0param0\\": \\"Red\\", - \\"updateProducts_args_update_colors0_disconnect0_disconnect_photos0_where_Photo_this_colors0_disconnect0_photos0param0\\": \\"123\\", - \\"updateProducts_args_update_colors0_disconnect0_disconnect_photos0_disconnect_color0_where_Color_this_colors0_disconnect0_photos0_color0param0\\": \\"134\\", - \\"updateProducts_args_update_photos0_disconnect0_where_Photo_this_photos0_disconnect0param0\\": \\"321\\", - \\"updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect0_color0param0\\": \\"Green\\", - \\"updateProducts_args_update_photos0_disconnect1_where_Photo_this_photos0_disconnect1param0\\": \\"33211\\", - \\"updateProducts_args_update_photos0_disconnect0_disconnect_color0_where_Color_this_photos0_disconnect1_color0param0\\": \\"Red\\", - \\"updateProducts\\": { - \\"args\\": { - \\"update\\": { - \\"id_SET\\": \\"123\\", - \\"name_SET\\": \\"Nested Connect\\", - \\"colors\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Red\\" - } - } - }, - \\"disconnect\\": { - \\"photos\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"123\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"134\\" - } - } - } - } - ] - } - } - ] - } - } - ] - } - ], - \\"photos\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"321\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Green\\" - } - } - } - } - ] - } - }, - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"33211\\" - } - } - }, - \\"disconnect\\": { - \\"color\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Red\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"123\\", + \\"param1\\": \\"Nested Connect\\", + \\"param2\\": \\"Red\\", + \\"param3\\": \\"123\\", + \\"param4\\": \\"134\\", + \\"param5\\": \\"321\\", + \\"param6\\": \\"Green\\", + \\"param7\\": \\"33211\\", + \\"param8\\": \\"Red\\" }" `); }); diff --git a/packages/graphql/tests/tck/operations/update.test.ts b/packages/graphql/tests/tck/operations/update.test.ts index 4defa093bf..964eb085e3 100644 --- a/packages/graphql/tests/tck/operations/update.test.ts +++ b/packages/graphql/tests/tck/operations/update.test.ts @@ -63,16 +63,18 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"2\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"2\\" }" `); }); @@ -106,46 +108,25 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_actors0.name = $this_update_actors0_name_SET - RETURN count(*) AS update_this_actors0 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2 } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"old name\\", - \\"this_update_actors0_name_SET\\": \\"new name\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"new name\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"old name\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"old name\\", + \\"param2\\": \\"new name\\" }" `); }); @@ -188,72 +169,35 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_actors0.name = $this_update_actors0_name_SET - WITH this, this_actors0 - CALL(*) { - WITH this, this_actors0 - MATCH (this_actors0)-[this_actors0_acted_in0_relationship:ACTED_IN]->(this_actors0_movies0:Movie) - WHERE this_actors0_movies0.id = $updateMovies_args_update_actors0_update_node_movies0_where_this_actors0_movies0param0 - SET this_actors0_movies0.title = $this_update_actors0_movies0_title_SET - RETURN count(*) AS update_this_actors0_movies0 - } - RETURN count(*) AS update_this_actors0 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2 + WITH * + CALL (*) { + MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WITH * + WHERE this3.id = $param3 + SET + this3.title = $param4 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"old actor name\\", - \\"this_update_actors0_name_SET\\": \\"new actor name\\", - \\"updateMovies_args_update_actors0_update_node_movies0_where_this_actors0_movies0param0\\": \\"old movie title\\", - \\"this_update_actors0_movies0_title_SET\\": \\"new movie title\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"new actor name\\", - \\"movies\\": [ - { - \\"update\\": { - \\"node\\": { - \\"title_SET\\": \\"new movie title\\" - }, - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"old movie title\\" - } - } - } - } - } - ] - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"old actor name\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"old actor name\\", + \\"param2\\": \\"new actor name\\", + \\"param3\\": \\"old movie title\\", + \\"param4\\": \\"new movie title\\" }" `); }); @@ -277,31 +221,24 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect0_node:Actor) - WHERE this_actors0_connect0_node.name = $this_actors0_connect0_node_param0 - CALL(*) { - WITH collect(this_actors0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect0_node - CREATE (this)<-[this_actors0_connect0_relationship:ACTED_IN]-(this_actors0_connect0_node) - } - } - WITH this, this_actors0_connect0_node - RETURN count(*) AS connect_this_actors0_connect_Actor0 + CALL (*) { + CALL (this) { + MATCH (this0:Actor) + WHERE this0.name = $param1 + CREATE (this)<-[this1:ACTED_IN]-(this0) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_actors0_connect0_node_param0\\": \\"Daniel\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\" }" `); }); @@ -332,48 +269,33 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect0_node:Actor) - WHERE this_actors0_connect0_node.name = $this_actors0_connect0_node_param0 - CALL(*) { - WITH collect(this_actors0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect0_node - CREATE (this)<-[this_actors0_connect0_relationship:ACTED_IN]-(this_actors0_connect0_node) - } - } - WITH this, this_actors0_connect0_node - RETURN count(*) AS connect_this_actors0_connect_Actor0 + CALL (*) { + CALL (this) { + MATCH (this0:Actor) + WHERE this0.name = $param1 + CREATE (this)<-[this1:ACTED_IN]-(this0) + } } WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_actors0_connect1_node:Actor) - WHERE this_actors0_connect1_node.name = $this_actors0_connect1_node_param0 - CALL(*) { - WITH collect(this_actors0_connect1_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_actors0_connect1_node - CREATE (this)<-[this_actors0_connect1_relationship:ACTED_IN]-(this_actors0_connect1_node) - } - } - WITH this, this_actors0_connect1_node - RETURN count(*) AS connect_this_actors0_connect_Actor1 + CALL (*) { + CALL (this) { + MATCH (this2:Actor) + WHERE this2.name = $param2 + CREATE (this)<-[this3:ACTED_IN]-(this2) + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_actors0_connect0_node_param0\\": \\"Daniel\\", - \\"this_actors0_connect1_node_param0\\": \\"Darrell\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\", + \\"param2\\": \\"Darrell\\" }" `); }); @@ -397,48 +319,25 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_actors0_disconnect0_rel:ACTED_IN]-(this_actors0_disconnect0:Actor) - WHERE this_actors0_disconnect0.name = $updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0 - CALL (this_actors0_disconnect0, this_actors0_disconnect0_rel, this) { - WITH collect(this_actors0_disconnect0) as this_actors0_disconnect0_x, this_actors0_disconnect0_rel, this - UNWIND this_actors0_disconnect0_x as x - DELETE this_actors0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_actors0_disconnect_Actor + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WHERE this1.name = $param1 + WITH * + DELETE this0 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0\\": \\"Daniel\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Daniel\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\" }" `); }); @@ -469,70 +368,35 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_actors0_disconnect0_rel:ACTED_IN]-(this_actors0_disconnect0:Actor) - WHERE this_actors0_disconnect0.name = $updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0 - CALL (this_actors0_disconnect0, this_actors0_disconnect0_rel, this) { - WITH collect(this_actors0_disconnect0) as this_actors0_disconnect0_x, this_actors0_disconnect0_rel, this - UNWIND this_actors0_disconnect0_x as x - DELETE this_actors0_disconnect0_rel + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WHERE this1.name = $param1 + WITH * + DELETE this0 + } } - RETURN count(*) AS disconnect_this_actors0_disconnect_Actor + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)<-[this2:ACTED_IN]-(this3:Actor) + WHERE this3.name = $param2 + WITH * + DELETE this2 + } } WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)<-[this_actors0_disconnect1_rel:ACTED_IN]-(this_actors0_disconnect1:Actor) - WHERE this_actors0_disconnect1.name = $updateMovies_args_update_actors0_disconnect1_where_Actor_this_actors0_disconnect1param0 - CALL (this_actors0_disconnect1, this_actors0_disconnect1_rel, this) { - WITH collect(this_actors0_disconnect1) as this_actors0_disconnect1_x, this_actors0_disconnect1_rel, this - UNWIND this_actors0_disconnect1_x as x - DELETE this_actors0_disconnect1_rel - } - RETURN count(*) AS disconnect_this_actors0_disconnect_Actor - } - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_disconnect0_where_Actor_this_actors0_disconnect0param0\\": \\"Daniel\\", - \\"updateMovies_args_update_actors0_disconnect1_where_Actor_this_actors0_disconnect1param0\\": \\"Darrell\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Daniel\\" - } - } - } - }, - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Darrell\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Daniel\\", + \\"param2\\": \\"Darrell\\" }" `); }); @@ -560,28 +424,31 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) + WITH * WHERE this.name = $param0 - WITH this - CREATE (this_movies0_create0_node:Movie) - SET this_movies0_create0_node.id = $this_movies0_create0_node_id - SET this_movies0_create0_node.title = $this_movies0_create0_node_title - MERGE (this)-[this_movies0_create0_relationship:ACTED_IN]->(this_movies0_create0_node) WITH * + CALL (*) { + CREATE (this0:Movie) + MERGE (this)-[this1:ACTED_IN]->(this0) + SET + this0.id = $param1, + this0.title = $param2 + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .id, .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .id, .title } AS this3 + RETURN collect(this3) AS var4 } - RETURN collect(DISTINCT this { .name, movies: update_var2 }) AS data" + RETURN this { .name, movies: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Dan\\", - \\"this_movies0_create0_node_id\\": \\"dan_movie_id\\", - \\"this_movies0_create0_node_title\\": \\"The Story of Beer\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"dan_movie_id\\", + \\"param2\\": \\"The Story of Beer\\" }" `); }); @@ -609,28 +476,31 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) + WITH * WHERE this.name = $param0 - WITH this - CREATE (this_movies0_create0_node:Movie) - SET this_movies0_create0_node.id = $this_movies0_create0_node_id - SET this_movies0_create0_node.title = $this_movies0_create0_node_title - MERGE (this)-[this_movies0_create0_relationship:ACTED_IN]->(this_movies0_create0_node) WITH * + CALL (*) { + CREATE (this0:Movie) + MERGE (this)-[this1:ACTED_IN]->(this0) + SET + this0.id = $param1, + this0.title = $param2 + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .id, .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH DISTINCT this3 + WITH this3 { .id, .title } AS this3 + RETURN collect(this3) AS var4 } - RETURN collect(DISTINCT this { .name, movies: update_var2 }) AS data" + RETURN this { .name, movies: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Dan\\", - \\"this_movies0_create0_node_id\\": \\"dan_movie_id\\", - \\"this_movies0_create0_node_title\\": \\"The Story of Beer\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"dan_movie_id\\", + \\"param2\\": \\"The Story of Beer\\" }" `); }); @@ -665,34 +535,41 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Actor) + WITH * WHERE this.name = $param0 - WITH this - CREATE (this_movies0_create0_node:Movie) - SET this_movies0_create0_node.id = $this_movies0_create0_node_id - SET this_movies0_create0_node.title = $this_movies0_create0_node_title - MERGE (this)-[this_movies0_create0_relationship:ACTED_IN]->(this_movies0_create0_node) - CREATE (this_movies0_create1_node:Movie) - SET this_movies0_create1_node.id = $this_movies0_create1_node_id - SET this_movies0_create1_node.title = $this_movies0_create1_node_title - MERGE (this)-[this_movies0_create1_relationship:ACTED_IN]->(this_movies0_create1_node) WITH * + CALL (*) { + CREATE (this0:Movie) + MERGE (this)-[this1:ACTED_IN]->(this0) + SET + this0.id = $param1, + this0.title = $param2 + } + WITH * + CALL (*) { + CREATE (this2:Movie) + MERGE (this)-[this3:ACTED_IN]->(this2) + SET + this2.id = $param3, + this2.title = $param4 + } + WITH this CALL (this) { - MATCH (this)-[update_this0:ACTED_IN]->(update_this1:Movie) - WITH DISTINCT update_this1 - WITH update_this1 { .id, .title } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this4:ACTED_IN]->(this5:Movie) + WITH DISTINCT this5 + WITH this5 { .id, .title } AS this5 + RETURN collect(this5) AS var6 } - RETURN collect(DISTINCT this { .name, movies: update_var2 }) AS data" + RETURN this { .name, movies: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Dan\\", - \\"this_movies0_create0_node_id\\": \\"dan_movie_id\\", - \\"this_movies0_create0_node_title\\": \\"The Story of Beer\\", - \\"this_movies0_create1_node_id\\": \\"dan_movie2_id\\", - \\"this_movies0_create1_node_title\\": \\"Forrest Gump\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"dan_movie_id\\", + \\"param2\\": \\"The Story of Beer\\", + \\"param3\\": \\"dan_movie2_id\\", + \\"param4\\": \\"Forrest Gump\\" }" `); }); @@ -722,58 +599,30 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors0_delete0_relationship:ACTED_IN]-(this_actors0_delete0:Actor) - WHERE (this_actors0_delete0.name = $updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0 AND this_actors0_delete0_relationship.screenTime = $updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param1) - WITH this_actors0_delete0_relationship, collect(DISTINCT this_actors0_delete0) AS this_actors0_delete0_to_delete - CALL(this_actors0_delete0_to_delete) { - UNWIND this_actors0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WHERE (this1.name = $param1 AND this0.screenTime = $param2) + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0\\": \\"Actor to delete\\", - \\"updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param1\\": { + \\"param1\\": \\"Actor to delete\\", + \\"param2\\": { \\"low\\": 60, \\"high\\": 0 - }, - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor to delete\\" - } - }, - \\"edge\\": { - \\"screenTime\\": { - \\"eq\\": { - \\"low\\": 60, - \\"high\\": 0 - } - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + } }" `); }); @@ -805,68 +654,36 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors0_delete0_relationship:ACTED_IN]-(this_actors0_delete0:Actor) - WHERE this_actors0_delete0.name = $updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0 - WITH this_actors0_delete0_relationship, collect(DISTINCT this_actors0_delete0) AS this_actors0_delete0_to_delete - CALL(this_actors0_delete0_to_delete) { - UNWIND this_actors0_delete0_to_delete AS x - DETACH DELETE x + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2 } + WITH * + CALL (*) { + OPTIONAL MATCH (this)<-[this2:ACTED_IN]-(this3:Actor) + WHERE this3.name = $param3 + WITH this2, collect(DISTINCT this3) AS var4 + CALL (var4) { + UNWIND var4 AS var5 + DETACH DELETE var5 + } } WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_actors0.name = $this_update_actors0_name_SET - RETURN count(*) AS update_this_actors0 - } - RETURN collect(DISTINCT this { .id }) AS data" + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0\\": \\"Actor to delete\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"Actor to update\\", - \\"this_update_actors0_name_SET\\": \\"Updated name\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"Updated name\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor to update\\" - } - } - } - }, - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor to delete\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Actor to update\\", + \\"param2\\": \\"Updated name\\", + \\"param3\\": \\"Actor to delete\\" }" `); }); @@ -890,46 +707,26 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors0_delete0_relationship:ACTED_IN]-(this_actors0_delete0:Actor) - WHERE this_actors0_delete0.name = $updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0 - WITH this_actors0_delete0_relationship, collect(DISTINCT this_actors0_delete0) AS this_actors0_delete0_to_delete - CALL(this_actors0_delete0_to_delete) { - UNWIND this_actors0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WHERE this1.name = $param1 + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0\\": \\"Actor to delete\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor to delete\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Actor to delete\\" }" `); }); @@ -960,70 +757,37 @@ describe("Cypher Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - WHERE this.id = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this)<-[this_actors0_delete0_relationship:ACTED_IN]-(this_actors0_delete0:Actor) - WHERE this_actors0_delete0.name = $updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0 + WHERE this.id = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this_actors0_delete0)-[this_actors0_delete0_movies0_relationship:ACTED_IN]->(this_actors0_delete0_movies0:Movie) - WHERE this_actors0_delete0_movies0.id = $updateMovies_args_update_actors0_delete0_delete_movies0_where_this_actors0_delete0_movies0param0 - WITH this_actors0_delete0_movies0_relationship, collect(DISTINCT this_actors0_delete0_movies0) AS this_actors0_delete0_movies0_to_delete - CALL(this_actors0_delete0_movies0_to_delete) { - UNWIND this_actors0_delete0_movies0_to_delete AS x - DETACH DELETE x - } - } - WITH this_actors0_delete0_relationship, collect(DISTINCT this_actors0_delete0) AS this_actors0_delete0_to_delete - CALL(this_actors0_delete0_to_delete) { - UNWIND this_actors0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WHERE this1.name = $param1 + WITH * + CALL (*) { + OPTIONAL MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WHERE this3.id = $param2 + WITH this2, collect(DISTINCT this3) AS var4 + CALL (var4) { + UNWIND var4 AS var5 + DETACH DELETE var5 + } + } + WITH this0, collect(DISTINCT this1) AS var6 + CALL (var6) { + UNWIND var6 AS var7 + DETACH DELETE var7 + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"updateMovies_args_update_actors0_delete0_where_this_actors0_delete0param0\\": \\"Actor to delete\\", - \\"updateMovies_args_update_actors0_delete0_delete_movies0_where_this_actors0_delete0_movies0param0\\": \\"2\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"actors\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Actor to delete\\" - } - } - }, - \\"delete\\": { - \\"movies\\": [ - { - \\"where\\": { - \\"node\\": { - \\"id\\": { - \\"eq\\": \\"2\\" - } - } - } - } - ] - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Actor to delete\\", + \\"param2\\": \\"2\\" }" `); }); diff --git a/packages/graphql/tests/tck/pringles.test.ts b/packages/graphql/tests/tck/pringles.test.ts index 29f68a263d..219fd73b11 100644 --- a/packages/graphql/tests/tck/pringles.test.ts +++ b/packages/graphql/tests/tck/pringles.test.ts @@ -239,102 +239,44 @@ describe("Cypher Create Pringles", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Product) + WITH * WHERE this.name = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_has_photo0_relationship:HAS_PHOTO]->(this_photos0:Photo) - WHERE this_photos0.description = $updateProducts_args_update_photos0_where_this_photos0param0 - SET this_photos0.description = $this_update_photos0_description_SET - WITH this, this_photos0 - CALL(*) { - WITH this, this_photos0 - OPTIONAL MATCH (this_photos0)-[this_photos0_color0_disconnect0_rel:OF_COLOR]->(this_photos0_color0_disconnect0:Color) - WHERE this_photos0_color0_disconnect0.name = $updateProducts_args_update_photos0_update_node_color0_disconnect0_where_Color_this_photos0_color0_disconnect0param0 - CALL (this_photos0_color0_disconnect0, this_photos0_color0_disconnect0_rel, this_photos0) { - WITH collect(this_photos0_color0_disconnect0) as this_photos0_color0_disconnect0_x, this_photos0_color0_disconnect0_rel, this_photos0 - UNWIND this_photos0_color0_disconnect0_x as x - DELETE this_photos0_color0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_photos0_color0_disconnect_Color - } - WITH * - CALL(*) { - WITH this, this_photos0 - OPTIONAL MATCH (this_photos0_color0_connect0_node:Color) - WHERE this_photos0_color0_connect0_node.name = $this_photos0_color0_connect0_node_param0 - CALL(*) { - WITH this, collect(this_photos0_color0_connect0_node) as connectedNodes, collect(this_photos0) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this_photos0 - UNWIND connectedNodes as this_photos0_color0_connect0_node - CREATE (this_photos0)-[:OF_COLOR]->(this_photos0_color0_connect0_node) - } - } - WITH this, this_photos0, this_photos0_color0_connect0_node - RETURN count(*) AS connect_this_photos0_color0_connect_Color0 - } - RETURN count(*) AS update_this_photos0 + WITH * + CALL (*) { + MATCH (this)-[this0:HAS_PHOTO]->(this1:Photo) + WITH * + WHERE this1.description = $param1 + SET + this1.description = $param2 + WITH * + CALL (*) { + CALL (this1) { + MATCH (this2:Color) + WHERE this2.name = $param3 + CREATE (this1)-[this3:OF_COLOR]->(this2) + } + } + WITH * + CALL (*) { + CALL (this1) { + OPTIONAL MATCH (this1)-[this4:OF_COLOR]->(this5:Color) + WHERE this5.name = $param4 + WITH * + DELETE this4 + } + } } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"Pringles\\", - \\"updateProducts_args_update_photos0_where_this_photos0param0\\": \\"Green Photo\\", - \\"this_update_photos0_description_SET\\": \\"Light Green Photo\\", - \\"updateProducts_args_update_photos0_update_node_color0_disconnect0_where_Color_this_photos0_color0_disconnect0param0\\": \\"Green\\", - \\"this_photos0_color0_connect0_node_param0\\": \\"Light Green\\", - \\"updateProducts\\": { - \\"args\\": { - \\"update\\": { - \\"photos\\": [ - { - \\"update\\": { - \\"node\\": { - \\"description_SET\\": \\"Light Green Photo\\", - \\"color\\": [ - { - \\"connect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Light Green\\" - } - } - } - } - ], - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Green\\" - } - } - } - } - ] - } - ] - }, - \\"where\\": { - \\"node\\": { - \\"description\\": { - \\"eq\\": \\"Green Photo\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Green Photo\\", + \\"param2\\": \\"Light Green Photo\\", + \\"param3\\": \\"Light Green\\", + \\"param4\\": \\"Green\\" }" `); }); diff --git a/packages/graphql/tests/tck/subscriptions/update.test.ts b/packages/graphql/tests/tck/subscriptions/update.test.ts index b73d028469..fb1ab49c73 100644 --- a/packages/graphql/tests/tck/subscriptions/update.test.ts +++ b/packages/graphql/tests/tck/subscriptions/update.test.ts @@ -62,16 +62,18 @@ describe("Subscriptions metadata on update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - RETURN collect(DISTINCT this { .id }) AS data" + SET + this.id = $param1 + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"2\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"2\\" }" `); }); @@ -100,49 +102,28 @@ describe("Subscriptions metadata on update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.id = $param0 - SET this.id = $this_update_id_SET - WITH this - CALL(*) { - WITH this - MATCH (this)<-[this_acted_in0_relationship:ACTED_IN]-(this_actors0:Actor) - WHERE this_actors0.name = $updateMovies_args_update_actors0_where_this_actors0param0 - SET this_actors0.name = $this_update_actors0_name_SET - RETURN count(*) AS update_this_actors0 + SET + this.id = $param1 + WITH * + CALL (*) { + MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH * + WHERE this1.name = $param2 + SET + this1.name = $param3 } - RETURN collect(DISTINCT this { .id }) AS data" + WITH this + RETURN this { .id } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"1\\", - \\"this_update_id_SET\\": \\"2\\", - \\"updateMovies_args_update_actors0_where_this_actors0param0\\": \\"arthur\\", - \\"this_update_actors0_name_SET\\": \\"ford\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"id_SET\\": \\"2\\", - \\"actors\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"ford\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"arthur\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"2\\", + \\"param2\\": \\"arthur\\", + \\"param3\\": \\"ford\\" }" `); }); diff --git a/packages/graphql/tests/tck/types/date.test.ts b/packages/graphql/tests/tck/types/date.test.ts index 93fd51325f..d6c1e24aa8 100644 --- a/packages/graphql/tests/tck/types/date.test.ts +++ b/packages/graphql/tests/tck/types/date.test.ts @@ -152,18 +152,20 @@ describe("Cypher Date", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.date = $this_update_date_SET - RETURN collect(DISTINCT this { .id, .date }) AS data" + WITH * + SET + this.date = $param0 + WITH this + RETURN this { .id, .date } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_date_SET\\": { + \\"param0\\": { \\"year\\": 1970, \\"month\\": 1, \\"day\\": 1 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/types/datetime.test.ts b/packages/graphql/tests/tck/types/datetime.test.ts index fbbeed03ac..4063daf8a6 100644 --- a/packages/graphql/tests/tck/types/datetime.test.ts +++ b/packages/graphql/tests/tck/types/datetime.test.ts @@ -115,14 +115,16 @@ describe("Cypher DateTime", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.datetime = datetime($this_update_datetime_SET) - RETURN collect(DISTINCT this { .id, datetime: apoc.date.convertFormat(toString(this.datetime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") }) AS data" + WITH * + SET + this.datetime = datetime($param0) + WITH this + RETURN this { .id, datetime: apoc.date.convertFormat(toString(this.datetime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_datetime_SET\\": \\"1970-01-01T00:00:00.000Z\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"1970-01-01T00:00:00.000Z\\" }" `); }); diff --git a/packages/graphql/tests/tck/types/duration.test.ts b/packages/graphql/tests/tck/types/duration.test.ts index 2b61ef4d1f..2be966d569 100644 --- a/packages/graphql/tests/tck/types/duration.test.ts +++ b/packages/graphql/tests/tck/types/duration.test.ts @@ -155,19 +155,21 @@ describe("Cypher Duration", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.duration = $this_update_duration_SET - RETURN collect(DISTINCT this { .id, .duration }) AS data" + WITH * + SET + this.duration = $param0 + WITH this + RETURN this { .id, .duration } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_duration_SET\\": { + \\"param0\\": { \\"months\\": 0, \\"days\\": 4, \\"seconds\\": 0, \\"nanoseconds\\": 0 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/types/localdatetime.test.ts b/packages/graphql/tests/tck/types/localdatetime.test.ts index 3a3c9c8ac0..d59a2e6328 100644 --- a/packages/graphql/tests/tck/types/localdatetime.test.ts +++ b/packages/graphql/tests/tck/types/localdatetime.test.ts @@ -164,13 +164,16 @@ describe("Cypher LocalDateTime", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.localDT = $this_update_localDT_SET - RETURN collect(DISTINCT this { .id, .localDT }) AS data" + WITH * + SET + this.localDT = $param0 + WITH this + RETURN this { .id, .localDT } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_localDT_SET\\": { + \\"param0\\": { \\"year\\": 1881, \\"month\\": 7, \\"day\\": 13, @@ -178,8 +181,7 @@ describe("Cypher LocalDateTime", () => { \\"minute\\": 24, \\"second\\": 40, \\"nanosecond\\": 845512000 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/types/localtime.test.ts b/packages/graphql/tests/tck/types/localtime.test.ts index 5199626e24..eae356d3a5 100644 --- a/packages/graphql/tests/tck/types/localtime.test.ts +++ b/packages/graphql/tests/tck/types/localtime.test.ts @@ -155,19 +155,21 @@ describe("Cypher LocalTime", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.time = $this_update_time_SET - RETURN collect(DISTINCT this { .id, .time }) AS data" + WITH * + SET + this.time = $param0 + WITH this + RETURN this { .id, .time } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_time_SET\\": { + \\"param0\\": { \\"hour\\": 9, \\"minute\\": 24, \\"second\\": 40, \\"nanosecond\\": 845512000 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/types/point.test.ts b/packages/graphql/tests/tck/types/point.test.ts index 1e2fef1dbb..c9bb3ece04 100644 --- a/packages/graphql/tests/tck/types/point.test.ts +++ b/packages/graphql/tests/tck/types/point.test.ts @@ -382,19 +382,21 @@ describe("Cypher Points", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:PointContainer) + WITH * WHERE this.id = $param0 - SET this.point = point($this_update_point_SET) - RETURN collect(DISTINCT this { .point }) AS data" + SET + this.point = point($param1) + WITH this + RETURN this { .point } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"id\\", - \\"this_update_point_SET\\": { + \\"param1\\": { \\"longitude\\": 1, \\"latitude\\": 2 - }, - \\"resolvedCallbacks\\": {} + } }" `); }); diff --git a/packages/graphql/tests/tck/types/points.test.ts b/packages/graphql/tests/tck/types/points.test.ts index ab663dc581..5fda6dd54d 100644 --- a/packages/graphql/tests/tck/types/points.test.ts +++ b/packages/graphql/tests/tck/types/points.test.ts @@ -204,21 +204,23 @@ describe("Cypher Points", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:PointContainer) + WITH * WHERE this.id = $param0 - SET this.points = [p in $this_update_points_SET | point(p)] - RETURN collect(DISTINCT this { .points }) AS data" + SET + this.points = [var0 IN $param1 | point(var0)] + WITH this + RETURN this { .points } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"id\\", - \\"this_update_points_SET\\": [ + \\"param1\\": [ { \\"longitude\\": 1, \\"latitude\\": 2 } - ], - \\"resolvedCallbacks\\": {} + ] }" `); }); diff --git a/packages/graphql/tests/tck/types/time.test.ts b/packages/graphql/tests/tck/types/time.test.ts index cf2fd26378..e74968c334 100644 --- a/packages/graphql/tests/tck/types/time.test.ts +++ b/packages/graphql/tests/tck/types/time.test.ts @@ -140,14 +140,16 @@ describe("Cypher Time", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) - SET this.time = time($this_update_time_SET) - RETURN collect(DISTINCT this { .id, .time }) AS data" + WITH * + SET + this.time = time($param0) + WITH this + RETURN this { .id, .time } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_update_time_SET\\": \\"09:24:40.845512+06:30\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"09:24:40.845512+06:30\\" }" `); }); diff --git a/packages/graphql/tests/tck/undirected-relationships/query-direction.test.ts b/packages/graphql/tests/tck/undirected-relationships/query-direction.test.ts index 2233939ce4..6e91d00e82 100644 --- a/packages/graphql/tests/tck/undirected-relationships/query-direction.test.ts +++ b/packages/graphql/tests/tck/undirected-relationships/query-direction.test.ts @@ -125,58 +125,34 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]->(this0:User) WHERE this0.name = $param0 } - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_friends0_disconnect0_rel:FRIENDS_WITH]->(this_friends0_disconnect0:User) - WHERE this_friends0_disconnect0.name = $updateUsers_args_update_friends0_disconnect0_where_User_this_friends0_disconnect0param0 - CALL (this_friends0_disconnect0, this_friends0_disconnect0_rel, this) { - WITH collect(this_friends0_disconnect0) as this_friends0_disconnect0_x, this_friends0_disconnect0_rel, this - UNWIND this_friends0_disconnect0_x as x - DELETE this_friends0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_friends0_disconnect_User - } WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this1:FRIENDS_WITH]->(this2:User) + WHERE this2.name = $param1 + WITH * + DELETE this1 + } + } + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]->(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this3:FRIENDS_WITH]->(this4:User) + WITH DISTINCT this4 + WITH this4 { .name } AS this4 + RETURN collect(this4) AS var5 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_disconnect0_where_User_this_friends0_disconnect0param0\\": \\"Jane Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\" }" `); }); @@ -203,56 +179,35 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]->(this0:User) WHERE this0.name = $param0 } WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_friends0_delete0_relationship:FRIENDS_WITH]->(this_friends0_delete0:User) - WHERE this_friends0_delete0.name = $updateUsers_args_update_friends0_delete0_where_this_friends0_delete0param0 - WITH this_friends0_delete0_relationship, collect(DISTINCT this_friends0_delete0) AS this_friends0_delete0_to_delete - CALL(this_friends0_delete0_to_delete) { - UNWIND this_friends0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)-[this1:FRIENDS_WITH]->(this2:User) + WHERE this2.name = $param1 + WITH this1, collect(DISTINCT this2) AS var3 + CALL (var3) { + UNWIND var3 AS var4 + DETACH DELETE var4 + } } - WITH * + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]->(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this5:FRIENDS_WITH]->(this6:User) + WITH DISTINCT this6 + WITH this6 { .name } AS this6 + RETURN collect(this6) AS var7 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_delete0_where_this_friends0_delete0param0\\": \\"Jane Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\" }" `); }); @@ -286,56 +241,34 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]->(this0:User) WHERE this0.name = $param0 } - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_friends_with0_relationship:FRIENDS_WITH]->(this_friends0:User) - WHERE this_friends0.name = $updateUsers_args_update_friends0_where_this_friends0param0 - SET this_friends0.name = $this_update_friends0_name_SET - RETURN count(*) AS update_this_friends0 - } WITH * + CALL (*) { + MATCH (this)-[this1:FRIENDS_WITH]->(this2:User) + WITH * + WHERE this2.name = $param1 + SET + this2.name = $param2 + } + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]->(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this3:FRIENDS_WITH]->(this4:User) + WITH DISTINCT this4 + WITH this4 { .name } AS this4 + RETURN collect(this4) AS var5 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_where_this_friends0param0\\": \\"Jane Smith\\", - \\"this_update_friends0_name_SET\\": \\"Janet Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"Janet Smith\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\", + \\"param2\\": \\"Janet Smith\\" }" `); }); @@ -488,58 +421,34 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]-(this0:User) WHERE this0.name = $param0 } - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_friends0_disconnect0_rel:FRIENDS_WITH]-(this_friends0_disconnect0:User) - WHERE this_friends0_disconnect0.name = $updateUsers_args_update_friends0_disconnect0_where_User_this_friends0_disconnect0param0 - CALL (this_friends0_disconnect0, this_friends0_disconnect0_rel, this) { - WITH collect(this_friends0_disconnect0) as this_friends0_disconnect0_x, this_friends0_disconnect0_rel, this - UNWIND this_friends0_disconnect0_x as x - DELETE this_friends0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_friends0_disconnect_User - } WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this1:FRIENDS_WITH]-(this2:User) + WHERE this2.name = $param1 + WITH * + DELETE this1 + } + } + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]-(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this3:FRIENDS_WITH]-(this4:User) + WITH DISTINCT this4 + WITH this4 { .name } AS this4 + RETURN collect(this4) AS var5 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_disconnect0_where_User_this_friends0_disconnect0param0\\": \\"Jane Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\" }" `); }); @@ -566,56 +475,35 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]-(this0:User) WHERE this0.name = $param0 } WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_friends0_delete0_relationship:FRIENDS_WITH]-(this_friends0_delete0:User) - WHERE this_friends0_delete0.name = $updateUsers_args_update_friends0_delete0_where_this_friends0_delete0param0 - WITH this_friends0_delete0_relationship, collect(DISTINCT this_friends0_delete0) AS this_friends0_delete0_to_delete - CALL(this_friends0_delete0_to_delete) { - UNWIND this_friends0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)-[this1:FRIENDS_WITH]-(this2:User) + WHERE this2.name = $param1 + WITH this1, collect(DISTINCT this2) AS var3 + CALL (var3) { + UNWIND var3 AS var4 + DETACH DELETE var4 + } } - WITH * + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]-(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this5:FRIENDS_WITH]-(this6:User) + WITH DISTINCT this6 + WITH this6 { .name } AS this6 + RETURN collect(this6) AS var7 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_delete0_where_this_friends0_delete0param0\\": \\"Jane Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - ] - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\" }" `); }); @@ -649,56 +537,34 @@ describe("queryDirection in relationships", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:User) + WITH * WHERE EXISTS { MATCH (this)-[:FRIENDS_WITH]-(this0:User) WHERE this0.name = $param0 } - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_friends_with0_relationship:FRIENDS_WITH]-(this_friends0:User) - WHERE this_friends0.name = $updateUsers_args_update_friends0_where_this_friends0param0 - SET this_friends0.name = $this_update_friends0_name_SET - RETURN count(*) AS update_this_friends0 - } WITH * + CALL (*) { + MATCH (this)-[this1:FRIENDS_WITH]-(this2:User) + WITH * + WHERE this2.name = $param1 + SET + this2.name = $param2 + } + WITH this CALL (this) { - MATCH (this)-[update_this0:FRIENDS_WITH]-(update_this1:User) - WITH DISTINCT update_this1 - WITH update_this1 { .name } AS update_this1 - RETURN collect(update_this1) AS update_var2 + MATCH (this)-[this3:FRIENDS_WITH]-(this4:User) + WITH DISTINCT this4 + WITH this4 { .name } AS this4 + RETURN collect(this4) AS var5 } - RETURN collect(DISTINCT this { .name, friends: update_var2 }) AS data" + RETURN this { .name, friends: var5 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"John Smith\\", - \\"updateUsers_args_update_friends0_where_this_friends0param0\\": \\"Jane Smith\\", - \\"this_update_friends0_name_SET\\": \\"Janet Smith\\", - \\"updateUsers\\": { - \\"args\\": { - \\"update\\": { - \\"friends\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"Janet Smith\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"Jane Smith\\" - } - } - } - } - } - ] - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"Jane Smith\\", + \\"param2\\": \\"Janet Smith\\" }" `); }); diff --git a/packages/graphql/tests/tck/union.test.ts b/packages/graphql/tests/tck/union.test.ts index 8fdabbe3ee..1918ffc45a 100644 --- a/packages/graphql/tests/tck/union.test.ts +++ b/packages/graphql/tests/tck/union.test.ts @@ -290,17 +290,21 @@ describe("Cypher Union", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * + WITH * + CALL (*) { + CREATE (this0:Genre) + MERGE (this)-[this1:SEARCH]->(this0) + SET + this0.name = $param0 + } WITH this - CREATE (this_search_Genre0_create0_node:Genre) - SET this_search_Genre0_create0_node.name = $this_search_Genre0_create0_node_name - MERGE (this)-[this_search_Genre0_create0_relationship:SEARCH]->(this_search_Genre0_create0_node) - RETURN collect(DISTINCT this { .title }) AS data" + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this_search_Genre0_create0_node_name\\": \\"some genre\\", - \\"resolvedCallbacks\\": {} + \\"param0\\": \\"some genre\\" }" `); }); @@ -335,6 +339,7 @@ describe("Cypher Union", () => { WITH * CALL (this0) { MATCH (this1:Genre) + WHERE this1.name = $param1 CREATE (this0)-[this2:SEARCH]->(this1) } RETURN this0 AS this @@ -348,7 +353,8 @@ describe("Cypher Union", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"param0\\": \\"some movie\\" + \\"param0\\": \\"some movie\\", + \\"param1\\": \\"some genre\\" }" `); }); @@ -382,48 +388,25 @@ describe("Cypher Union", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL(*) { - WITH this - MATCH (this)-[this_search0_relationship:SEARCH]->(this_search_Genre0:Genre) - WHERE this_search_Genre0.name = $updateMovies_args_update_search_Genre0_where_this_search_Genre0param0 - SET this_search_Genre0.name = $this_update_search_Genre0_name_SET - RETURN count(*) AS update_this_search_Genre0 + WITH * + CALL (*) { + MATCH (this)-[this0:SEARCH]->(this1:Genre) + WITH * + WHERE this1.name = $param1 + SET + this1.name = $param2 } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"some movie\\", - \\"updateMovies_args_update_search_Genre0_where_this_search_Genre0param0\\": \\"some genre\\", - \\"this_update_search_Genre0_name_SET\\": \\"some new genre\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"search\\": { - \\"Genre\\": [ - { - \\"update\\": { - \\"node\\": { - \\"name_SET\\": \\"some new genre\\" - }, - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"some genre\\" - } - } - } - } - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"some genre\\", + \\"param2\\": \\"some new genre\\" }" `); }); @@ -448,50 +431,25 @@ describe("Cypher Union", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 - WITH this - CALL(*) { - WITH this - OPTIONAL MATCH (this)-[this_search_Genre0_disconnect0_rel:SEARCH]->(this_search_Genre0_disconnect0:Genre) - WHERE this_search_Genre0_disconnect0.name = $updateMovies_args_update_search_Genre0_disconnect0_where_Genre_this_search_Genre0_disconnect0param0 - CALL (this_search_Genre0_disconnect0, this_search_Genre0_disconnect0_rel, this) { - WITH collect(this_search_Genre0_disconnect0) as this_search_Genre0_disconnect0_x, this_search_Genre0_disconnect0_rel, this - UNWIND this_search_Genre0_disconnect0_x as x - DELETE this_search_Genre0_disconnect0_rel - } - RETURN count(*) AS disconnect_this_search_Genre0_disconnect_Genre + WITH * + CALL (*) { + CALL (this) { + OPTIONAL MATCH (this)-[this0:SEARCH]->(this1:Genre) + WHERE this1.name = $param1 + WITH * + DELETE this0 + } } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"some movie\\", - \\"updateMovies_args_update_search_Genre0_disconnect0_where_Genre_this_search_Genre0_disconnect0param0\\": \\"some genre\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"search\\": { - \\"Genre\\": [ - { - \\"disconnect\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"some genre\\" - } - } - } - } - ] - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"some genre\\" }" `); }); @@ -516,31 +474,24 @@ describe("Cypher Union", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 WITH * - CALL(*) { - WITH this - OPTIONAL MATCH (this_search_Genre0_connect0_node:Genre) - WHERE this_search_Genre0_connect0_node.name = $this_search_Genre0_connect0_node_param0 - CALL(*) { - WITH collect(this_search_Genre0_connect0_node) as connectedNodes, collect(this) as parentNodes - CALL(connectedNodes, parentNodes) { - UNWIND parentNodes as this - UNWIND connectedNodes as this_search_Genre0_connect0_node - CREATE (this)-[:SEARCH]->(this_search_Genre0_connect0_node) - } - } - WITH this, this_search_Genre0_connect0_node - RETURN count(*) AS connect_this_search_Genre0_connect_Genre0 + CALL (*) { + CALL (this) { + MATCH (this0:Genre) + WHERE this0.name = $param1 + CREATE (this)-[this1:SEARCH]->(this0) + } } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"some movie\\", - \\"this_search_Genre0_connect0_node_param0\\": \\"some genre\\", - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"some genre\\" }" `); }); @@ -565,48 +516,26 @@ describe("Cypher Union", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "CYPHER 5 MATCH (this:Movie) + WITH * WHERE this.title = $param0 WITH * - CALL(*) { - OPTIONAL MATCH (this)-[this_search_Genre0_delete0_relationship:SEARCH]->(this_search_Genre0_delete0:Genre) - WHERE this_search_Genre0_delete0.name = $updateMovies_args_update_search0_delete_Genre0_where_this_search_Genre0_delete0param0 - WITH this_search_Genre0_delete0_relationship, collect(DISTINCT this_search_Genre0_delete0) AS this_search_Genre0_delete0_to_delete - CALL(this_search_Genre0_delete0_to_delete) { - UNWIND this_search_Genre0_delete0_to_delete AS x - DETACH DELETE x - } + CALL (*) { + OPTIONAL MATCH (this)-[this0:SEARCH]->(this1:Genre) + WHERE this1.name = $param1 + WITH this0, collect(DISTINCT this1) AS var2 + CALL (var2) { + UNWIND var2 AS var3 + DETACH DELETE var3 + } } - RETURN collect(DISTINCT this { .title }) AS data" + WITH this + RETURN this { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"some movie\\", - \\"updateMovies_args_update_search0_delete_Genre0_where_this_search_Genre0_delete0param0\\": \\"some genre\\", - \\"updateMovies\\": { - \\"args\\": { - \\"update\\": { - \\"search\\": { - \\"Genre\\": [ - { - \\"delete\\": [ - { - \\"where\\": { - \\"node\\": { - \\"name\\": { - \\"eq\\": \\"some genre\\" - } - } - } - } - ] - } - ] - } - } - } - }, - \\"resolvedCallbacks\\": {} + \\"param1\\": \\"some genre\\" }" `); }); diff --git a/packages/graphql/tests/utils/builders/context-builder.ts b/packages/graphql/tests/utils/builders/context-builder.ts index 335a918931..cbdfaad286 100644 --- a/packages/graphql/tests/utils/builders/context-builder.ts +++ b/packages/graphql/tests/utils/builders/context-builder.ts @@ -32,8 +32,6 @@ export class ContextBuilder extends Builder = {}) { super({ resolveTree: {} as ResolveTree, - nodes: [], - relationships: [], schemaModel: new Neo4jGraphQLSchemaModel({ concreteEntities: [] as ConcreteEntity[], compositeEntities: [] as CompositeEntity[], diff --git a/packages/graphql/tests/utils/builders/node-builder.ts b/packages/graphql/tests/utils/builders/node-builder.ts deleted file mode 100644 index 6581292d93..0000000000 --- a/packages/graphql/tests/utils/builders/node-builder.ts +++ /dev/null @@ -1,62 +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 { NodeConstructor } from "../../../src/classes"; -import { Node } from "../../../src/classes"; -import type { NodeDirectiveConstructor } from "../../../src/classes/NodeDirective"; -import { NodeDirective } from "../../../src/classes/NodeDirective"; -import { Builder } from "./builder"; - -export class NodeBuilder extends Builder { - constructor(newOptions: Partial = {}) { - super({ - name: "", - relationFields: [], - connectionFields: [], - cypherFields: [], - primitiveFields: [], - scalarFields: [], - enumFields: [], - otherDirectives: [], - propagatedDirectives: [], - unionFields: [], - interfaceFields: [], - interfaces: [], - objectFields: [], - temporalFields: [], - pointFields: [], - customResolverFields: [], - ...newOptions, - }); - } - - public with(newOptions: Partial): NodeBuilder { - this.options = { ...this.options, ...newOptions }; - return this; - } - - public withNodeDirective(directiveOptions: NodeDirectiveConstructor): NodeBuilder { - const nodeDirective = new NodeDirective(directiveOptions); - return this.with({ nodeDirective }); - } - - public instance(): Node { - return new Node(this.options); - } -}