diff --git a/.changeset/cypher-builder-patch.md b/.changeset/cypher-builder-patch.md new file mode 100644 index 0000000000..3b241f7991 --- /dev/null +++ b/.changeset/cypher-builder-patch.md @@ -0,0 +1,6 @@ +--- +"@neo4j/cypher-builder": patch +--- + +Included List, date, localtime, localdatetime, time, randomUUID. +It's possible now to set edge properties from the Merge clause. \ No newline at end of file diff --git a/.changeset/unwind-create.md b/.changeset/unwind-create.md new file mode 100644 index 0000000000..5fd66e5e21 --- /dev/null +++ b/.changeset/unwind-create.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": minor +--- + +Optimized batch creation, when possible, to improve performance when a large numbers of nodes are created in single mutation diff --git a/docs/modules/ROOT/content-nav.adoc b/docs/modules/ROOT/content-nav.adoc index e0e94d3de8..bf1060697e 100644 --- a/docs/modules/ROOT/content-nav.adoc +++ b/docs/modules/ROOT/content-nav.adoc @@ -88,6 +88,7 @@ ** xref:troubleshooting/index.adoc[] *** xref:troubleshooting/faqs.adoc[] *** xref:troubleshooting/security.adoc[] +*** xref:troubleshooting/optimizing-create-operations.adoc[] ** xref:appendix/index.adoc[] *** xref:appendix/preventing-overfetching.adoc[] ** xref:deprecations.adoc[Deprecations] diff --git a/docs/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc b/docs/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc new file mode 100644 index 0000000000..6e01026c40 --- /dev/null +++ b/docs/modules/ROOT/pages/troubleshooting/optimizing-create-operations.adoc @@ -0,0 +1,23 @@ +[[optimizing-create-operations]] += Optimizing create operations + +It's possible to use the Neo4jGraphQL library to create several nodes and relationships in a single mutation, however, +it's well known that performance issues are present in performing this task. +A solution has been implemented that doesn't require any changes from the user. +However, there are still several situations where this kind of optimization is not achievable. + +== Subscriptions enabled + +No optimizations are available if a Subscription plugin it's being used. + +== `@populated_by` + +No optimizations are available if a Node affected by the mutation has a field with the directive `@populated_by`. + +== `@auth` + +No optimizations are available if a Node or Field affected by the mutation is secured with the directive `@auth`. + +== `connect` and `connectOrCreate` operations + +No optimizations are available if the GraphQL input contains the `connect` or `connectOrCreate` operation. diff --git a/packages/cypher-builder/src/Cypher.ts b/packages/cypher-builder/src/Cypher.ts index 9b6bfcab49..c7234f146e 100644 --- a/packages/cypher-builder/src/Cypher.ts +++ b/packages/cypher-builder/src/Cypher.ts @@ -22,6 +22,7 @@ export { Match, OptionalMatch } from "./clauses/Match"; export { Create } from "./clauses/Create"; export { Merge } from "./clauses/Merge"; export { Call } from "./clauses/Call"; +export { CallProcedure } from "./clauses/CallProcedure"; export { Return } from "./clauses/Return"; export { RawCypher } from "./clauses/RawCypher"; export { With } from "./clauses/With"; @@ -49,6 +50,7 @@ export * as apoc from "./expressions/procedures/apoc/apoc"; // --Lists export { ListComprehension } from "./expressions/list/ListComprehension"; export { PatternComprehension } from "./expressions/list/PatternComprehension"; +export { ListExpr as List } from "./expressions/list/ListExpr"; // --Map export { MapExpr as Map } from "./expressions/map/MapExpr"; @@ -79,12 +81,17 @@ export { distance, pointDistance, cypherDatetime as datetime, + cypherDate as date, + cypherLocalTime as localtime, + cypherLocalDatetime as localdatetime, + cypherTime as time, labels, count, min, max, avg, sum, + randomUUID } from "./expressions/functions/CypherFunction"; export * from "./expressions/functions/ListFunctions"; export { any, all, exists, single } from "./expressions/functions/PredicateFunctions"; @@ -109,5 +116,5 @@ export type { CompositeClause } from "./clauses/utils/concat"; import { escapeLabel } from "./utils/escape-label"; export const utils = { - escapeLabel, + escapeLabel }; diff --git a/packages/cypher-builder/src/clauses/CallProcedure.ts b/packages/cypher-builder/src/clauses/CallProcedure.ts new file mode 100644 index 0000000000..2c61840c80 --- /dev/null +++ b/packages/cypher-builder/src/clauses/CallProcedure.ts @@ -0,0 +1,36 @@ +/* + * 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 { CypherEnvironment } from "../Environment"; +import { Clause } from "./Clause"; +import type { Procedure } from "../types"; + +export class CallProcedure extends Clause { + private procedure: Procedure; + + constructor(procedure: Procedure) { + super(); + this.procedure = procedure; + } + + public getCypher(env: CypherEnvironment): string { + const procedureCypher = this.procedure.getCypher(env); + return `CALL ${procedureCypher}`; + } +} diff --git a/packages/cypher-builder/src/clauses/Merge.ts b/packages/cypher-builder/src/clauses/Merge.ts index 59d3d5b21c..0ee01e86b9 100644 --- a/packages/cypher-builder/src/clauses/Merge.ts +++ b/packages/cypher-builder/src/clauses/Merge.ts @@ -25,11 +25,12 @@ import { Clause } from "./Clause"; import { OnCreate, OnCreateParam } from "./sub-clauses/OnCreate"; import { WithReturn } from "./mixins/WithReturn"; import { mixin } from "./utils/mixin"; +import { WithSet } from "./mixins/WithSet"; +import { compileCypherIfExists } from "../utils/compile-cypher-if-exists"; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface Merge extends WithReturn {} +export interface Merge extends WithReturn, WithSet {} -@mixin(WithReturn) +@mixin(WithReturn, WithSet) export class Merge extends Clause { private pattern: Pattern; private onCreateClause: OnCreate; @@ -54,6 +55,7 @@ export class Merge extends Clause { public getCypher(env: CypherEnvironment): string { const mergeStr = `MERGE ${this.pattern.getCypher(env)}`; + const setCypher = compileCypherIfExists(this.setSubClause, env, { prefix: "\n" }); const onCreateStatement = this.onCreateClause.getCypher(env); const separator = onCreateStatement ? "\n" : ""; @@ -62,6 +64,6 @@ export class Merge extends Clause { returnCypher = `\n${this.returnStatement.getCypher(env)}`; } - return `${mergeStr}${separator}${onCreateStatement}${returnCypher}`; + return `${mergeStr}${separator}${setCypher}${onCreateStatement}${returnCypher}`; } } diff --git a/packages/cypher-builder/src/clauses/Unwind.test.ts b/packages/cypher-builder/src/clauses/Unwind.test.ts new file mode 100644 index 0000000000..7aae75f066 --- /dev/null +++ b/packages/cypher-builder/src/clauses/Unwind.test.ts @@ -0,0 +1,34 @@ +/* + * 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 Cypher from "../Cypher"; + +describe("CypherBuilder UNWIND", () => { + test("UNWIND Movies", () => { + const matrix = new Cypher.Map({ title: new Cypher.Literal("Matrix") }); + const matrix2 = new Cypher.Map({ title: new Cypher.Literal("Matrix 2") }); + const moviesList = new Cypher.List([matrix, matrix2]); + const unwindQuery = new Cypher.Unwind([moviesList, "batch"]); + const queryResult = unwindQuery.build(); + expect(queryResult.cypher).toMatchInlineSnapshot( + `"UNWIND [ { title: \\"Matrix\\" }, { title: \\"Matrix 2\\" } ] AS batch"` + ); + expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); + }); +}); diff --git a/packages/cypher-builder/src/expressions/functions/CypherFunction.ts b/packages/cypher-builder/src/expressions/functions/CypherFunction.ts index 8d197d1bf7..367429de93 100644 --- a/packages/cypher-builder/src/expressions/functions/CypherFunction.ts +++ b/packages/cypher-builder/src/expressions/functions/CypherFunction.ts @@ -68,6 +68,24 @@ export function cypherDatetime(): CypherFunction { return new CypherFunction("datetime"); } +// TODO: Add optional input to date functions - https://neo4j.com/docs/cypher-manual/current/functions/#header-query-functions-temporal-instant-types + +export function cypherDate(): CypherFunction { + return new CypherFunction("date"); +} + +export function cypherLocalDatetime(): CypherFunction { + return new CypherFunction("localdatetime"); +} + +export function cypherLocalTime(): CypherFunction { + return new CypherFunction("localtime"); +} + +export function cypherTime(): CypherFunction { + return new CypherFunction("time"); +} + export function count(expr: Expr): CypherFunction { return new CypherFunction("count", [expr]); } @@ -87,3 +105,9 @@ export function avg(expr: Expr): CypherFunction { export function sum(expr: Expr): CypherFunction { return new CypherFunction("sum", [expr]); } + +export function randomUUID(): CypherFunction { + return new CypherFunction("randomUUID"); +} + + diff --git a/packages/cypher-builder/src/expressions/list/ListExpr.ts b/packages/cypher-builder/src/expressions/list/ListExpr.ts new file mode 100644 index 0000000000..22812adcd2 --- /dev/null +++ b/packages/cypher-builder/src/expressions/list/ListExpr.ts @@ -0,0 +1,44 @@ +/* + * 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 { CypherEnvironment } from "../../Environment"; +import type { CypherCompilable, Expr } from "../../types"; + + +/** Represents a List */ +export class ListExpr implements CypherCompilable { + private value: Expr[]; + + constructor(value: Expr[]) { + this.value = value; + } + + private serializeList(env: CypherEnvironment, obj: Expr[]): string { + const valuesList = obj.map(expr => { + return expr.getCypher(env); + }); + + const serializedContent = valuesList.join(", "); + return `[ ${serializedContent} ]`; + } + + public getCypher(env: CypherEnvironment): string { + return this.serializeList(env, this.value); + } +} diff --git a/packages/cypher-builder/src/expressions/map/MapProjection.test.ts b/packages/cypher-builder/src/expressions/map/MapProjection.test.ts index b5fa105e8f..54d1aec02a 100644 --- a/packages/cypher-builder/src/expressions/map/MapProjection.test.ts +++ b/packages/cypher-builder/src/expressions/map/MapProjection.test.ts @@ -31,20 +31,6 @@ describe("Map Projection", () => { expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); }); - test("Project map with variables and nodes in projection", () => { - const var1 = new Cypher.Variable(); - const var2 = new Cypher.NamedVariable("NamedVar"); - const node = new Cypher.Node({}); - - const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), [var1, var2, node]); - - const queryResult = new TestClause(mapProjection).build(); - - expect(queryResult.cypher).toMatchInlineSnapshot(`"var0 { .var1, .NamedVar, .this2 }"`); - - expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); - }); - test("Project map with extra values only", () => { const var1 = new Cypher.Variable(); const var2 = new Cypher.NamedVariable("NamedVar"); @@ -61,35 +47,31 @@ describe("Map Projection", () => { expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); }); - test("Project map with variables in projection and extra values", () => { - const var1 = new Cypher.Variable(); - const var2 = new Cypher.NamedVariable("NamedVar"); + test("Project map with properties in projection and extra values", () => { const node = new Cypher.Node({}); - const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), [var1, var2], { + const mapProjection = new Cypher.MapProjection(new Cypher.Variable(), [".title", ".name"], { namedValue: Cypher.count(node), }); const queryResult = new TestClause(mapProjection).build(); - expect(queryResult.cypher).toMatchInlineSnapshot(`"var0 { .var2, .NamedVar, namedValue: count(this1) }"`); + expect(queryResult.cypher).toMatchInlineSnapshot(`"var0 { .title, .name, namedValue: count(this1) }"`); expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); }); test("Map Projection in return", () => { const mapVar = new Cypher.Variable(); - const var1 = new Cypher.Variable(); - const var2 = new Cypher.NamedVariable("NamedVar"); const node = new Cypher.Node({}); - const mapProjection = new Cypher.MapProjection(mapVar, [var1, var2], { + const mapProjection = new Cypher.MapProjection(mapVar, [".title", ".name"], { namedValue: Cypher.count(node), }); const queryResult = new Cypher.Return([mapProjection, mapVar]).build(); expect(queryResult.cypher).toMatchInlineSnapshot( - `"RETURN var0 { .var2, .NamedVar, namedValue: count(this1) } AS var0"` + `"RETURN var0 { .title, .name, namedValue: count(this1) } AS var0"` ); expect(queryResult.params).toMatchInlineSnapshot(`Object {}`); diff --git a/packages/cypher-builder/src/expressions/map/MapProjection.ts b/packages/cypher-builder/src/expressions/map/MapProjection.ts index 13c88e2487..db6f833bbb 100644 --- a/packages/cypher-builder/src/expressions/map/MapProjection.ts +++ b/packages/cypher-builder/src/expressions/map/MapProjection.ts @@ -19,26 +19,26 @@ import type { CypherEnvironment } from "../../Environment"; import type { CypherCompilable, Expr } from "../../types"; +import type { Variable } from "../../variables/Variable"; import { serializeMap } from "../../utils/serialize-map"; -import { Variable } from "../../variables/Variable"; /** Represents a Map projection https://neo4j.com/docs/cypher-manual/current/syntax/maps/#cypher-map-projection */ export class MapProjection implements CypherCompilable { private extraValues: Record; private variable: Variable; - private projection: Array; + private projection: string[]; - constructor(variable: Variable, projection: Array, extraValues: Record = {}) { + constructor(variable: Variable, projection: string[], extraValues: Record = {}) { this.variable = variable; this.projection = projection; this.extraValues = extraValues; } - public set(values: Record | Variable): void { - if (values instanceof Variable) { - this.projection.push(values); + public set(values: Record | string): void { + if (values instanceof String) { + this.projection.push(values as string); } else { - this.extraValues = { ...this.extraValues, ...values }; + this.extraValues = { ...this.extraValues, ...values as Record }; } } @@ -46,7 +46,7 @@ export class MapProjection implements CypherCompilable { const variableStr = this.variable.getCypher(env); const extraValuesStr = serializeMap(env, this.extraValues, true); - const projectionStr = this.projection.map((v) => `.${v.getCypher(env)}`).join(", "); + const projectionStr = this.projection.join(", "); const commaStr = extraValuesStr && projectionStr ? ", " : ""; diff --git a/packages/cypher-builder/src/types.ts b/packages/cypher-builder/src/types.ts index 9d1fe4f508..49d79dbb8b 100644 --- a/packages/cypher-builder/src/types.ts +++ b/packages/cypher-builder/src/types.ts @@ -27,11 +27,12 @@ import type { BooleanOp } from "./expressions/operations/boolean"; import type { ComparisonOp } from "./expressions/operations/comparison"; import type { RawCypher } from "./clauses/RawCypher"; import type { PredicateFunction } from "./expressions/functions/PredicateFunctions"; -import type { ApocExpr, ApocPredicate } from "./expressions/procedures/apoc/apoc"; +import type { ApocExpr, ApocPredicate, ValidatePredicate } from "./expressions/procedures/apoc/apoc"; import type { Case } from "./expressions/Case"; import type { MathOp } from "./expressions/operations/math"; import type { ListComprehension } from "./expressions/list/ListComprehension"; import type { PatternComprehension } from "./expressions/list/PatternComprehension"; +import type { ListExpr } from "./expressions/list/ListExpr"; import type { MapProjection } from "./expressions/map/MapProjection"; import type { HasLabel } from "./expressions/HasLabel"; import type { Reference } from "./variables/Reference"; @@ -49,6 +50,7 @@ export type Expr = | PatternComprehension | MapExpr // NOTE this cannot be set as a property in a node | MapProjection // NOTE this cannot be set as a property in a node + | ListExpr | ApocExpr; /** Represents a predicate statement (i.e returns a boolean). Note that RawCypher is only added for compatibility */ @@ -63,6 +65,9 @@ export type Predicate = | Case | HasLabel; +/** Represents a procedure invocable with the CALL statement */ +export type Procedure = ValidatePredicate; + export type CypherResult = { cypher: string; params: Record; diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts new file mode 100644 index 0000000000..3b9e0fdebd --- /dev/null +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts @@ -0,0 +1,32 @@ +/* + * 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 { IAST, Visitor } from "./types"; +import { randomUUID } from 'crypto'; + +export abstract class AST implements IAST { + id = randomUUID(); + children: IAST[] = []; + + addChildren(node: IAST): void { + this.children.push(node); + } + + abstract accept(visitor: Visitor): void +} diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts new file mode 100644 index 0000000000..fd1829f564 --- /dev/null +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts @@ -0,0 +1,37 @@ +/* + * 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 { Visitor, ICreateAST } from "./types"; +import type { Node } from "../../../classes"; +import { AST } from "./AST"; + +export class CreateAST extends AST implements ICreateAST { + nodeProperties: string[]; + node: Node; + + constructor(nodeProperties: string[], node: Node) { + super(); + this.nodeProperties = nodeProperties; + this.node = node; + } + + accept(visitor: Visitor): void { + visitor.visitCreate(this); + } +} diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts new file mode 100644 index 0000000000..b697050283 --- /dev/null +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AST } from "./AST"; +import { CreateAST } from "./CreateAST"; +import { NestedCreateAST } from "./NestedCreateAST"; + +export { AST, CreateAST, NestedCreateAST }; +export type { IAST, IConnectAST, IConnectOrCreateAST, ICreateAST, INestedCreateAST, Visitor } from "./types"; diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts new file mode 100644 index 0000000000..f4a2c6bf0f --- /dev/null +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts @@ -0,0 +1,56 @@ +/* + * 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 { RelationField } from "../../../types"; +import type { Visitor, INestedCreateAST } from "./types"; +import type { Node, Relationship } from "../../../classes"; +import { AST } from "./AST"; + +export class NestedCreateAST extends AST implements INestedCreateAST { + node: Node; + parent: Node; + nodeProperties: string[]; + edgeProperties: string[]; + relationshipPropertyPath: string; + relationship: [RelationField | undefined, Node[]]; + edge: Relationship | undefined; + + constructor( + node: Node, + parent: Node, + nodeProperties: string[], + edgeProperties: string[], + relationshipPropertyPath: string, + relationship: [RelationField | undefined, Node[]], + edge?: Relationship + ) { + super(); + this.node = node; + this.parent = parent; + this.nodeProperties = nodeProperties; + this.edgeProperties = edgeProperties; + this.relationshipPropertyPath = relationshipPropertyPath; + this.relationship = relationship; + this.edge = edge; + } + + accept(visitor: Visitor): void { + visitor.visitNestedCreate(this); + } +} diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts new file mode 100644 index 0000000000..bcae7a6525 --- /dev/null +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts @@ -0,0 +1,67 @@ +/* + * 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 { TreeDescriptor } from "../types"; +import type { RelationField } from "../../../types"; +import type { Node, Relationship } from "../../../classes"; + +export interface IAST { + id: string; + children: IAST[]; + addChildren: (children: IAST) => void; + accept: (visitor: Visitor) => void; +} + +export interface IConnectAST extends IAST { + node: Node; + parent: Node; + edgeProperties: string[]; + where: TreeDescriptor; + connect: TreeDescriptor; + relationshipPropertyPath: string; + relationship: [RelationField | undefined, Node[]]; +} + +export interface IConnectOrCreateAST extends IAST { + parent: Node; + where: TreeDescriptor; + onCreate: TreeDescriptor; +} + +export interface ICreateAST extends IAST { + nodeProperties: string[]; + node: Node; +} + +export interface INestedCreateAST extends IAST { + node: Node; + parent: Node; + nodeProperties: string[]; + edgeProperties: string[]; + relationshipPropertyPath: string; + relationship: [RelationField | undefined, Node[]]; + edge: Relationship | undefined; +} + +export interface Visitor { + visitCreate: (create: ICreateAST) => void; + visitNestedCreate: (nestedCreate: INestedCreateAST) => void; + // visitConnect: (connect: IConnectAST) => void; + // visitConnectOrCreate: (connectOrCreate: IConnectOrCreateAST) => void; +} diff --git a/packages/graphql/src/translate/batch-create/README.md b/packages/graphql/src/translate/batch-create/README.md new file mode 100644 index 0000000000..9c0e3f8522 --- /dev/null +++ b/packages/graphql/src/translate/batch-create/README.md @@ -0,0 +1,91 @@ +# Unwind-create optimization + +The unwind-create optimization provides a solution to compress in an efficient way repeated Cypher statements in a more concise query. +As the name suggests, the current implementation affects only the Create mutation. +More details at: https://github.com/neo4j/graphql/blob/dev/docs/rfcs/rfc-024-unwind-create.md + +## Optimisation example + +### Not optimized query + +``` +CALL { +CREATE (this0:Movie) +SET this0.id = $this0_id +WITH this0 +WITH this0 +CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored +} +RETURN this0 +} +CALL { +CREATE (this1:Movie) +SET this1.id = $this1_id +WITH this1 +WITH this1 +CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored +} +RETURN this1 +} +RETURN [ this0 { .id }, this1 { .id }] AS data +``` + +### Optimised query + +``` +UNWIND [ { id: $create_param0 }, { id: $create_param1 } ] AS create_var1 +CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 +} +RETURN collect(create_this0 { .id }) AS data +``` + +## Implementation + +The unwind-create optimization is built on top of different phases. + +### Parse the GraphQLCreateInput to obtain the unique TreeDescriptor + +This phase consists in parsing the GraphQLCreateInput to obtain a TreeDescriptor that holds all the operations and the properties impacted by the operation. +This phase provides an early mechanism to identify operations not yet supported by the optimization. +The TreeDescriptor keeps a clear separation between scalar properties and nested operations. + +### Parse the TreeDescriptor to obtain an GraphQLInputAST + +With the TreeDescriptor already obtained, it's built a very simple Intermediate Representation named GraphQLInputAST. + +### Parse the GraphQLCreateInput to obtain the UNWIND statement + +At this phase, GraphQLCreateInput is translated into the UNWIND Cypher statement using the CypherBuilder. + +### Visit the GraphQLInputAST with the UnwindCrateVisitor + +The UnwindCreateVisitor traverses the GraphQLInputAST and generates all the nodes and edges described in the GraphQLInputAST. +The final output obtained from the UnwindCreateVisitor generates a single Cypher variable rather than one for any Nodes created in the Mutation, this means that the client needs to translate the SelectionSet accordingly. + + +If at any phase the optimization is no longer achievable an `UnsupportedUnwindOptimization` is raised. + + diff --git a/packages/graphql/src/translate/batch-create/parser.test.ts b/packages/graphql/src/translate/batch-create/parser.test.ts new file mode 100644 index 0000000000..d0cacd834e --- /dev/null +++ b/packages/graphql/src/translate/batch-create/parser.test.ts @@ -0,0 +1,374 @@ +/* + * 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 { mergeTreeDescriptors, getTreeDescriptor } from "./parser"; +import { Neo4jGraphQL } from "../../../src"; +import { gql } from "apollo-server"; +import type { GraphQLCreateInput } from "./types"; +import type Node from "../../classes/Node"; +import { ContextBuilder } from "../../../tests/utils/builders/context-builder"; +import { int } from "neo4j-driver"; + +describe("TreeDescriptor Parser", () => { + let typeDefs; + let movieNode; + let context; + let schema; + let nodes; + let relationships; + + beforeAll(async () => { + typeDefs = gql` + type Actor { + id: ID! @id + name: String + age: Int + height: Int + create: BigInt + edge: CartesianPoint + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type Movie { + id: ID + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Website { + create: BigInt + edge: CartesianPoint + address: String + } + + interface ActedIn @relationshipProperties { + year: Int + create: BigInt + edge: CartesianPoint + } + `; + const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { enableRegex: true }, + }); + schema = await neoSchema.getSchema(); + nodes = neoSchema.nodes; + relationships = neoSchema.relationships; + + movieNode = nodes.find((node) => node.name === "Movie") as unknown as Node; + context = new ContextBuilder({ + neoSchema: { nodes, relationships }, + nodes, + relationships, + schema, + }).instance(); + }); + + test("it should be possible to parse the GraphQL input to obtain a TreeDescriptor", () => { + const graphQLInput = { + id: 3, + website: { + create: { + node: { + address: "www.xyz.com", + }, + }, + }, + actors: { + create: [ + { + node: { + name: "Keanu", + }, + edge: { + year: 1992, + }, + }, + ], + }, + }; + const movieNode = nodes.find((node) => node.name === "Movie") as unknown as Node; + const context = new ContextBuilder({ + neoSchema: { nodes, relationships }, + nodes, + relationships, + schema, + }).instance(); + const treeDescriptor = Array.isArray(graphQLInput) + ? mergeTreeDescriptors(graphQLInput.map((el: GraphQLCreateInput) => getTreeDescriptor(el, movieNode, context))) + : getTreeDescriptor(graphQLInput, movieNode, context); + + expect(treeDescriptor).toEqual({ + properties: new Set(["id"]), + children: { + website: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["address"]), + children: {}, + }, + }, + }, + }, + }, + actors: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["name"]), + children: {}, + }, + edge: { + properties: new Set(["year"]), + children: {}, + }, + }, + }, + }, + }, + }, + }); + }); + + test("it should possible to obtain homogenous TreeDescriptor from an heterogeneous GraphQL input", () => { + const graphQLInput = { + id: 3, + website: { + create: { + node: { + address: "www.xyz.com", + }, + }, + }, + actors: { + connect: { + where: { + node: { + id: "123", + }, + }, + }, + connectOrCreate: { + where: { + node: { + id: "124", + }, + }, + onCreate: { + node: { + name: "Steven", + }, + }, + }, + create: [ + { + node: { + name: "Keanu", + }, + edge: { + year: 1992, + }, + }, + { + node: { + age: 60, + }, + edge: { + year: 1992, + }, + }, + { + node: { + height: 190, + }, + edge: { + year: 1992, + }, + }, + ], + }, + }; + + const treeDescriptor = Array.isArray(graphQLInput) + ? mergeTreeDescriptors(graphQLInput.map((el: GraphQLCreateInput) => getTreeDescriptor(el, movieNode, context))) + : getTreeDescriptor(graphQLInput, movieNode, context); + + expect(treeDescriptor).toEqual({ + properties: new Set(["id"]), + children: { + website: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["address"]), + children: {}, + }, + }, + }, + }, + }, + actors: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["name", "height", "age"]), + children: {}, + }, + edge: { + properties: new Set(["year"]), + children: {}, + }, + }, + }, + connect: { + properties: new Set(), + children: { + where: { + properties: new Set(), + children: { + node: { + properties: new Set(["id"]), + children: {}, + }, + }, + }, + }, + }, + connectOrCreate: { + properties: new Set(), + children: { + where: { + properties: new Set(), + children: { + node: { + properties: new Set(["id"]), + children: {}, + }, + }, + }, + onCreate: { + properties: new Set(), + children: { + node: { + properties: new Set(["name"]), + children: {}, + }, + }, + }, + }, + }, + }, + }, + }, + }); + }); + + test("it should works for property with reserved name", () => { + const graphQLInput = { + id: 3, + actors: { + create: [ + { + node: { + name: "Keanu", + create: int(123), + edge: { x: 10, y: 10 }, + website: { + create: { + node: { + create: int(123), + edge: { x: 10, y: 10 }, + }, + }, + }, + }, + edge: { + year: 1992, + create: int(123), + edge: { x: 10, y: 10 }, + }, + }, + ], + }, + }; + const movieNode = nodes.find((node) => node.name === "Movie") as unknown as Node; + const context = new ContextBuilder({ + neoSchema: { nodes, relationships }, + nodes, + relationships, + schema, + }).instance(); + const treeDescriptor = Array.isArray(graphQLInput) + ? mergeTreeDescriptors(graphQLInput.map((el: GraphQLCreateInput) => getTreeDescriptor(el, movieNode, context))) + : getTreeDescriptor(graphQLInput, movieNode, context); + + expect(treeDescriptor).toEqual({ + properties: new Set(["id"]), + children: { + actors: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["name", "create", "edge"]), + children: { + website: { + properties: new Set(), + children: { + create: { + properties: new Set(), + children: { + node: { + properties: new Set(["create", "edge"]), + children: {}, + }, + }, + }, + }, + }, + }, + }, + edge: { + properties: new Set(["year", "create", "edge"]), + children: {}, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/packages/graphql/src/translate/batch-create/parser.ts b/packages/graphql/src/translate/batch-create/parser.ts new file mode 100644 index 0000000000..fd0d1cc4bd --- /dev/null +++ b/packages/graphql/src/translate/batch-create/parser.ts @@ -0,0 +1,315 @@ +/* + * 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 { Context, RelationField } from "../../types"; +import type { GraphQLCreateInput, TreeDescriptor } from "./types"; +import { UnsupportedUnwindOptimization } from "./types"; +import { GraphElement, Neo4jGraphQLError, Node, Relationship } from "../../classes"; +import Cypher from "@neo4j/cypher-builder"; +import { AST, CreateAST, NestedCreateAST } from "./GraphQLInputAST/GraphQLInputAST"; +import mapToDbProperty from "../../utils/map-to-db-property"; + +function getRelationshipFields( + node: Node, + key: string, + value: any, + context: Context +): [RelationField | undefined, Node[]] { + const relationField = node.relationFields.find((x) => key === x.fieldName); + const refNodes: Node[] = []; + + if (relationField) { + if (relationField.union) { + Object.keys(value as Record).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); + } + } + return [relationField, refNodes]; +} + +export function inputTreeToCypherMap( + input: GraphQLCreateInput[] | GraphQLCreateInput, + node: Node, + context: Context, + parentKey?: string, + relationship?: Relationship +): Cypher.List | Cypher.Map { + if (Array.isArray(input)) { + return new Cypher.List( + input.map((GraphQLCreateInput: GraphQLCreateInput) => + inputTreeToCypherMap(GraphQLCreateInput, node, context, parentKey, relationship) + ) + ); + } + const properties = (Object.entries(input) as GraphQLCreateInput).reduce( + (obj: Record, [key, value]: [string, Record]) => { + const [relationField, relatedNodes] = getRelationshipFields(node, key, {}, context); + if (relationField && relationField.properties) { + relationship = context.relationships.find( + (x) => x.properties === relationField.properties + ) as unknown as Relationship; + } + let scalar = false; + if (parentKey === "edge") { + scalar = isScalar(key, relationship as Relationship); + } + // it assume that if parentKey is not defined then it means that the key belong to a Node + else if (parentKey === "node" || parentKey === undefined) { + scalar = isScalar(key, node); + } + if (typeof value === "object" && value !== null && (relationField || !scalar)) { + if (Array.isArray(value)) { + obj[key] = new Cypher.List( + value.map((GraphQLCreateInput: GraphQLCreateInput) => + inputTreeToCypherMap( + GraphQLCreateInput, + relationField ? relatedNodes[0] : node, + context, + key, + relationship + ) + ) + ); + return obj; + } + obj[key] = inputTreeToCypherMap( + value as GraphQLCreateInput[] | GraphQLCreateInput, + relationField ? relatedNodes[0] : node, + context, + key, + relationship + ) as Cypher.Map; + return obj; + } + obj[key] = new Cypher.Param(value); + return obj; + }, + {} as Record + ) as Record; + return new Cypher.Map(properties); +} + +function isScalar(fieldName: string, graphElement: GraphElement) { + const scalarPredicate = (x) => x.fieldName === fieldName; + const scalarFields = [ + graphElement.primitiveFields, + graphElement.temporalFields, + graphElement.pointFields, + graphElement.scalarFields, + ]; + return scalarFields.flat().some(scalarPredicate); +} + +export function getTreeDescriptor( + input: GraphQLCreateInput, + node: Node, + context: Context, + parentKey?: string, + relationship?: Relationship +): TreeDescriptor { + return Object.entries(input).reduce( + (previous, [key, value]) => { + const [relationField, relatedNodes] = getRelationshipFields(node, key, value, context); + if (relationField && relationField.properties) { + relationship = context.relationships.find( + (x) => x.properties === relationField.properties + ) as unknown as Relationship; + } + + let scalar = false; + if (parentKey === "edge") { + scalar = isScalar(key, relationship as Relationship); + } + // it assume that if parentKey is not defined then it means that the key belong to a Node + else if (parentKey === "node" || parentKey === undefined) { + scalar = isScalar(key, node); + } + if (typeof value === "object" && value !== null && !scalar) { + // TODO: supports union/interfaces + const innerNode = relationField ? relatedNodes[0] : node; + if (Array.isArray(value)) { + previous.children[key] = mergeTreeDescriptors( + value.map((el) => getTreeDescriptor(el as GraphQLCreateInput, innerNode, context, key, relationship)) + ); + return previous; + } + previous.children[key] = getTreeDescriptor( + value as GraphQLCreateInput, + innerNode, + context, + key, + relationship + ); + return previous; + } + previous.properties.add(key); + return previous; + }, + { properties: new Set(), children: {} } as TreeDescriptor + ); +} + +export function mergeTreeDescriptors(input: TreeDescriptor[]): TreeDescriptor { + return input.reduce( + (previous: TreeDescriptor, node: TreeDescriptor) => { + previous.properties = new Set([...previous.properties, ...node.properties]); + const entries = [...new Set([...Object.keys(previous.children), ...Object.keys(node.children)])].map( + (childrenKey) => { + const previousChildren: TreeDescriptor = + previous.children[childrenKey] ?? ({ properties: new Set(), children: {} } as TreeDescriptor); + const nodeChildren: TreeDescriptor = + node.children[childrenKey] ?? ({ properties: new Set(), children: {} } as TreeDescriptor); + return [childrenKey, mergeTreeDescriptors([previousChildren, nodeChildren])]; + } + ); + previous.children = Object.fromEntries(entries); + return previous; + }, + { properties: new Set(), children: {} } as TreeDescriptor + ); +} + +function parser(input: TreeDescriptor, node: Node, context: Context, parentASTNode: AST): AST { + if (node.auth) { + throw new UnsupportedUnwindOptimization("Not supported operation: Auth"); + } + Object.entries(input.children).forEach(([key, value]) => { + const [relationField, relatedNodes] = getRelationshipFields(node, key, {}, context); + + if (relationField) { + let edge; + if (relationField.properties) { + edge = context.relationships.find( + (x) => x.properties === relationField.properties + ) as unknown as Relationship; + } + if (relationField.interface || relationField.union) { + throw new UnsupportedUnwindOptimization(`Not supported operation: Interface or Union`); + } + Object.entries(value.children).forEach(([operation, description]) => { + switch (operation) { + case "create": + parentASTNode.addChildren( + parseNestedCreate( + description, + relatedNodes[0], + context, + node, + key, + [relationField, relatedNodes], + edge + ) + ); + break; + /* + case "connect": + parentASTNode.addChildren( + parseConnect(description, relatedNodes[0], context, node, key, [ + relationField, + relatedNodes, + ]) + ); + break; + case "connectOrCreate": + parentASTNode.addChildren(parseConnectOrCreate(description, relatedNodes[0], context, node)); + break; + */ + default: + throw new UnsupportedUnwindOptimization(`Not supported operation: ${operation}`); + } + }); + } + }); + return parentASTNode; +} + +function raiseAttributeAmbiguity(properties: Set | Array, graphElement: GraphElement) { + const hash = {}; + properties.forEach((property) => { + const dbName = mapToDbProperty(graphElement, property); + if (hash[dbName]) { + throw new Neo4jGraphQLError( + `Conflicting modification of ${[hash[dbName], property].map((n) => `[[${n}]]`).join(", ")} on type ${ + graphElement.name + }` + ); + } + hash[dbName] = property; + }); +} + +function raiseOnNotSupportedProperty(graphElement: GraphElement) { + graphElement.primitiveFields.forEach((property) => { + if (property.callback && property.callback.operations.includes("CREATE")) { + throw new UnsupportedUnwindOptimization("Not supported operation: Callback"); + } + if (property.auth) { + throw new UnsupportedUnwindOptimization("Not supported operation: Auth"); + } + }); +} + +export function parseCreate(input: TreeDescriptor, node: Node, context: Context) { + const nodeProperties = input.properties; + raiseOnNotSupportedProperty(node); + raiseAttributeAmbiguity(input.properties, node); + const createAST = new CreateAST([...nodeProperties], node); + parser(input, node, context, createAST); + return createAST; +} + +function parseNestedCreate( + input: TreeDescriptor, + node: Node, + context: Context, + parentNode: Node, + relationshipPropertyPath: string, + relationship: [RelationField | undefined, Node[]], + edge?: Relationship +) { + const nodeProperties = input.children.node.properties; + const edgeProperties = input.children.edge ? input.children.edge.properties : []; + raiseOnNotSupportedProperty(node); + raiseAttributeAmbiguity(nodeProperties, node); + if (edge) { + raiseOnNotSupportedProperty(edge); + raiseAttributeAmbiguity(edgeProperties, edge); + } + + const nestedCreateAST = new NestedCreateAST( + node, + parentNode, + [...nodeProperties], + [...edgeProperties], + relationshipPropertyPath, + relationship, + edge + ); + if (input.children.node) { + parser(input.children.node, node, context, nestedCreateAST); + } + return nestedCreateAST; +} diff --git a/packages/graphql/src/translate/batch-create/types.ts b/packages/graphql/src/translate/batch-create/types.ts new file mode 100644 index 0000000000..669f727c2a --- /dev/null +++ b/packages/graphql/src/translate/batch-create/types.ts @@ -0,0 +1,39 @@ +/* + * 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 type GraphQLCreateInput = Record; + +export interface TreeDescriptor { + properties: Set; + children: Record; + path: string; +} + +export class UnsupportedUnwindOptimization extends Error { + readonly name; + + constructor(message: string) { + super(message); + + // if no name provided, use the default. defineProperty ensures that it stays non-enumerable + if (!this.name) { + Object.defineProperty(this, "name", { value: "UnsupportedUnwindOptimization" }); + } + } +} diff --git a/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts b/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts new file mode 100644 index 0000000000..943c9034c7 --- /dev/null +++ b/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts @@ -0,0 +1,254 @@ +/* + * 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 { Context } from "../../../types"; +import type { CallbackBucket } from "../../../classes/CallbackBucket"; +import type { Visitor, ICreateAST, INestedCreateAST } from "../GraphQLInputAST/GraphQLInputAST"; +import type { Node, Relationship } from "../../../classes"; +import createRelationshipValidationString from "../../create-relationship-validation-string"; +import { filterTruthy } from "../../../utils/utils"; +import { Neo4jGraphQLError } from "../../../classes"; +import Cypher, { Expr, Map, MapProjection } from "@neo4j/cypher-builder"; +import mapToDbProperty from "../../../utils/map-to-db-property"; + +type UnwindCreateScopeDefinition = { + unwindVar: Cypher.Variable; + parentVar: Cypher.Variable; + clause?: Cypher.Clause; +} +type GraphQLInputASTNodeRef = string; +type UnwindCreateEnvironment = Record + +export class UnwindCreateVisitor implements Visitor { + unwindVar: Cypher.Variable; + callbackBucket: CallbackBucket; + context: Context; + rootNode: Cypher.Node | undefined; + clause: Cypher.Clause | undefined; + environment: UnwindCreateEnvironment; + + + constructor( + unwindVar: Cypher.Variable, + callbackBucket: CallbackBucket, + context: Context, + ) { + this.unwindVar = unwindVar; + this.callbackBucket = callbackBucket; + this.context = context; + this.environment = {}; + } + + visitCreate(create: ICreateAST): void { + const labels = create.node.getLabels(this.context); + const currentNode = new Cypher.Node({ + labels, + }); + + const setProperties = [...create.nodeProperties].map((property: string) => + fieldToSetParam(create.node, currentNode, property, this.unwindVar.property(property)) + ); + const autogeneratedProperties = getAutoGeneratedFields(create.node, currentNode); + + const createClause = new Cypher.Create(currentNode).set(...setProperties, ...autogeneratedProperties); + + const relationshipValidationClause = new Cypher.RawCypher((env: Cypher.Environment) => { + const validationStr = createRelationshipValidationString({ + node: create.node, + context: this.context, + varName: env.getReferenceId(currentNode), + }); + const cypher = [] as string[]; + + if (validationStr) { + cypher.push(`WITH ${env.getReferenceId(currentNode)}`); + cypher.push(validationStr); + } + return cypher.join("\n"); + }); + let nestedClauses; + if (create.children) { + const childrenRefs = create.children.map((children) => { + this.environment[children.id] = { unwindVar: this.unwindVar, parentVar: currentNode }; + children.accept(this); + return children.id; + }); + nestedClauses = childrenRefs.map((childrenRef) => this.environment[childrenRef].clause); + } + this.rootNode = currentNode; + const clause = Cypher.concat( + ...filterTruthy([ + createClause, + ...nestedClauses, + relationshipValidationClause, + new Cypher.Return(currentNode), + ]) + ); + this.clause = new Cypher.Call(clause).innerWith(this.unwindVar); + } + + visitNestedCreate(nestedCreate: INestedCreateAST): void { + const parentVar = this.environment[nestedCreate.id].parentVar; + const unwindVar = this.environment[nestedCreate.id].unwindVar; + if (!parentVar) throw new Neo4jGraphQLError("Generic Error"); + const { node, relationship, relationshipPropertyPath } = nestedCreate; + const blockWith = new Cypher.With(parentVar, unwindVar); + const createUnwindVar = new Cypher.Variable(); + const createUnwindClause = new Cypher.Unwind([ + unwindVar.property(relationshipPropertyPath).property("create"), + createUnwindVar, + ]); + const labels = node.getLabels(this.context); + const currentNode = new Cypher.Node({ + labels, + }); + const nodeVar = new Cypher.Variable(); + const edgeVar = new Cypher.Variable(); + const withCreate = new Cypher.With( + [createUnwindVar.property("node"), nodeVar], + [createUnwindVar.property("edge"), edgeVar], + parentVar + ); + const createClause = new Cypher.Create(currentNode); + if (!relationship[0]) { + throw new Neo4jGraphQLError("Nested created nodes should belong to a parent"); + } + + const relationshipClause = new Cypher.Relationship({ + source: currentNode, + target: parentVar as Cypher.Node, + type: relationship[0].type, + }); + + if (relationship[0].direction === "OUT") { + relationshipClause.reverse(); + } + + const mergeClause = new Cypher.Merge(relationshipClause); + + const setPropertiesNode = nestedCreate.nodeProperties.map((property: string) => + fieldToSetParam(node, currentNode, property, nodeVar.property(property)) + ); + const autogeneratedProperties = getAutoGeneratedFields(node, currentNode); + + createClause.set(...setPropertiesNode, ...autogeneratedProperties); + if (nestedCreate.edgeProperties && nestedCreate.edgeProperties.length && nestedCreate.edge) { + const setPropertiesEdge = nestedCreate.edgeProperties.map((property) => { + return fieldToSetParam( + nestedCreate.edge as Relationship, + relationshipClause, + property, + edgeVar.property(property) + ); + }); + const autogeneratedEdgeProperties = getAutoGeneratedFields(nestedCreate.edge, relationshipClause); + mergeClause.set(...setPropertiesEdge, ...autogeneratedEdgeProperties); + } + + const subQueryStatements = [blockWith, createUnwindClause, withCreate, createClause, mergeClause] as ( + | undefined + | Cypher.Clause + )[]; + const relationshipValidationClause = new Cypher.RawCypher((env: Cypher.Environment) => { + const validationStr = createRelationshipValidationString({ + node, + context: this.context, + varName: env.getReferenceId(currentNode), + }); + const cypher = [] as string[]; + if (validationStr) { + cypher.push(`WITH ${env.getReferenceId(currentNode)}`); + cypher.push(validationStr); + } + return cypher.join("\n"); + }); + + let nestedClauses; + + if (nestedCreate.children) { + const childrenRefs = nestedCreate.children.map((children) => { + this.environment[children.id] = { unwindVar: nodeVar, parentVar: currentNode }; + children.accept(this); + return children.id; + }); + nestedClauses = childrenRefs.map((childrenRef) => this.environment[childrenRef].clause); + subQueryStatements.push(...nestedClauses); + } + subQueryStatements.push(relationshipValidationClause); + subQueryStatements.push(new Cypher.Return(Cypher.collect(new Cypher.Literal(null)))); + const subQuery = Cypher.concat(...subQueryStatements); + const callClause = new Cypher.Call(subQuery); + const outsideWith = new Cypher.With(parentVar, unwindVar); + this.environment[nestedCreate.id].clause = Cypher.concat(outsideWith, callClause); + } + + /* + * Returns the Cypher Reference of the root Nodes and the Cypher Clause generated + */ + build(): [Cypher.Node?, Cypher.Clause?] { + return [this.rootNode, this.clause]; + } +} + +function getAutoGeneratedFields( + graphQLElement: Node | Relationship, + cypherNodeRef: Cypher.Node | Cypher.Relationship +): Cypher.SetParam[] { + const setParams: Cypher.SetParam[] = []; + const timestampedFields = graphQLElement.temporalFields.filter( + (x) => ["DateTime", "Time"].includes(x.typeMeta.name) && x.timestamps?.includes("CREATE") + ); + timestampedFields.forEach((field) => { + // DateTime -> datetime(); Time -> time() + const relatedCypherExpression = Cypher[field.typeMeta.name.toLowerCase()]() as Cypher.Expr; + setParams.push([ + cypherNodeRef.property(field.dbPropertyName as string), + relatedCypherExpression, + ] as Cypher.SetParam); + }); + + const autogeneratedIdFields = graphQLElement.primitiveFields.filter((x) => x.autogenerate); + autogeneratedIdFields.forEach((field) => { + setParams.push([ + cypherNodeRef.property(field.dbPropertyName as string), + Cypher.randomUUID(), + ] as Cypher.SetParam); + }); + return setParams; +} + +function fieldToSetParam( + graphQLElement: Node | Relationship, + cypherNodeRef: Cypher.Node | Cypher.Relationship, + key: string, + value: Exclude +): Cypher.SetParam { + const pointField = graphQLElement.pointFields.find((x) => key === x.fieldName); + const dbName = mapToDbProperty(graphQLElement, key); + if (pointField) { + if (pointField.typeMeta.array) { + const comprehensionVar = new Cypher.Variable(); + const mapPoint = Cypher.point(comprehensionVar); + const expression = new Cypher.ListComprehension(comprehensionVar, value).map(mapPoint); + return [cypherNodeRef.property(dbName), expression]; + } + return [cypherNodeRef.property(dbName), Cypher.point(value)]; + } + return [cypherNodeRef.property(dbName), value]; +} diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index 5a9d46d8e2..644290946a 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -25,6 +25,8 @@ import { AUTH_FORBIDDEN_ERROR, META_CYPHER_VARIABLE } from "../constants"; import { filterTruthy } from "../utils/utils"; import { CallbackBucket } from "../classes/CallbackBucket"; import Cypher from "@neo4j/cypher-builder"; +import unwindCreate from "./unwind-create"; +import { UnsupportedUnwindOptimization } from "./batch-create/types"; export default async function translateCreate({ context, @@ -33,8 +35,17 @@ export default async function translateCreate({ context: Context; node: Node; }): Promise<{ cypher: string; params: Record }> { + try { + return await unwindCreate({ context, node }); + } catch (error) { + if (!(error instanceof UnsupportedUnwindOptimization)) { + throw error; + } + } + const { resolveTree } = context; const mutationInputs = resolveTree.args.input as any[]; + const connectionStrs: string[] = []; const interfaceStrs: string[] = []; const projectionWith: string[] = []; @@ -52,9 +63,9 @@ export default async function translateCreate({ (res, input, index) => { const varName = `this${index}`; const create = [`CALL {`]; - const withVars = [varName]; projectionWith.push(varName); + if (context.subscriptionsEnabled) { create.push(`WITH [] AS ${META_CYPHER_VARIABLE}`); withVars.push(META_CYPHER_VARIABLE); @@ -70,8 +81,8 @@ export default async function translateCreate({ topLevelNodeVariable: varName, callbackBucket, }); - create.push(`${createAndParams[0]}`); + if (context.subscriptionsEnabled) { const metaVariable = `${varName}_${META_CYPHER_VARIABLE}`; create.push(`RETURN ${varName}, ${META_CYPHER_VARIABLE} AS ${metaVariable}`); @@ -79,12 +90,10 @@ export default async function translateCreate({ } else { create.push(`RETURN ${varName}`); } - + create.push(`}`); - res.createStrs.push(create.join("\n")); res.params = { ...res.params, ...createAndParams[1] }; - return res; }, { createStrs: [], params: {}, withVars: [] } @@ -110,17 +119,18 @@ export default async function translateCreate({ resolveTree: nodeProjection, varName: "REPLACE_ME", }); + projectionSubquery = Cypher.concat(...projection.subqueriesBeforeSort, ...projection.subqueries); if (projection.meta?.authValidateStrs?.length) { projAuth = `CALL apoc.util.validate(NOT (${projection.meta.authValidateStrs.join( " AND " )}), "${AUTH_FORBIDDEN_ERROR}", [0])`; } - + replacedProjectionParams = Object.entries(projection.params).reduce((res, [key, value]) => { return { ...res, [key.replace("REPLACE_ME", "projection")]: value }; }, {}); - + projectionStr = createStrs .map( (_, i) => @@ -194,7 +204,6 @@ export default async function translateCreate({ .replace(/REPLACE_ME/g, `this${i}`); }) : []; - const cypher = filterTruthy([ `${createStrs.join("\n")}`, projectionWithStr, @@ -206,7 +215,6 @@ export default async function translateCreate({ ]) .filter(Boolean) .join("\n"); - return [ cypher, { @@ -219,7 +227,6 @@ export default async function translateCreate({ }); const createQueryCypher = createQuery.build("create_"); - const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({ cypher: createQueryCypher.cypher, }); @@ -232,7 +239,6 @@ export default async function translateCreate({ }, }; } - function generateCreateReturnStatement(projectionStr: string | undefined, subscriptionsEnabled: boolean): string { const statements: string[] = []; diff --git a/packages/graphql/src/translate/unwind-create.ts b/packages/graphql/src/translate/unwind-create.ts new file mode 100644 index 0000000000..e024da494f --- /dev/null +++ b/packages/graphql/src/translate/unwind-create.ts @@ -0,0 +1,179 @@ +/* + * 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 } from "../classes"; +import type { Context } from "../types"; +import type { GraphQLCreateInput } from "./batch-create/types"; +import { UnsupportedUnwindOptimization } from "./batch-create/types"; +import { mergeTreeDescriptors, getTreeDescriptor, parseCreate } from "./batch-create/parser"; +import { UnwindCreateVisitor } from "./batch-create/unwind-create-visitors/UnwindCreateVisitor"; +import createProjectionAndParams from "./create-projection-and-params"; +import { META_CYPHER_VARIABLE } from "../constants"; +import { filterTruthy } from "../utils/utils"; +import { CallbackBucket } from "../classes/CallbackBucket"; +import Cypher from "@neo4j/cypher-builder"; +import { compileCypherIfExists } from "../utils/compile-cypher-if-exists"; + +export default async function unwindCreate({ + context, + node, +}: { + context: Context; + node: Node; +}): Promise<{ cypher: string; params: Record }> { + if (context.subscriptionsEnabled) { + throw new UnsupportedUnwindOptimization("Unwind create optimisation does not yet support subscriptions"); + } + const { resolveTree } = context; + const input = resolveTree.args.input as GraphQLCreateInput | GraphQLCreateInput[]; + const treeDescriptor = Array.isArray(input) + ? mergeTreeDescriptors(input.map((el: GraphQLCreateInput) => getTreeDescriptor(el, node, context))) + : getTreeDescriptor(input, node, context); + const createNodeAST = parseCreate(treeDescriptor, node, context); + const callbackBucket = new CallbackBucket(context); + const unwindVar = new Cypher.Variable(); + const unwind = new Cypher.Param(input); + const unwindQuery = new Cypher.Unwind([unwind, unwindVar]); + const unwindCreateVisitor = new UnwindCreateVisitor(unwindVar, callbackBucket, context); + createNodeAST.accept(unwindCreateVisitor); + const [rootNodeVariable, createCypher] = unwindCreateVisitor.build() as [Cypher.Node, Cypher.Clause]; + + const connectionStrs: string[] = []; + const interfaceStrs: string[] = []; + const projectionWith: string[] = []; + const mutationResponse = resolveTree.fieldsByTypeName[node.mutationResponseTypeNames.create]; + const nodeProjection = Object.values(mutationResponse).find((field) => field.name === node.plural); + const metaNames: string[] = []; + let replacedProjectionParams: Record = {}; + let projectionCypher: Cypher.Expr | undefined; + let authCalls: string | undefined; + + if (metaNames.length > 0) { + projectionWith.push(`${metaNames.join(" + ")} AS meta`); + } + + let projectionSubquery: Cypher.Clause | undefined; + if (nodeProjection) { + const projection = createProjectionAndParams({ + node, + context, + resolveTree: nodeProjection, + varName: "REPLACE_ME", + }); + projectionSubquery = Cypher.concat(...projection.subqueries); + + replacedProjectionParams = Object.entries(projection.params).reduce((res, [key, value]) => { + return { ...res, [key.replace("REPLACE_ME", "projection")]: value }; + }, {}); + + projectionCypher = new Cypher.RawCypher((env: Cypher.Environment) => { + return `${rootNodeVariable.getCypher(env)} ${projection.projection + // First look to see if projection param is being reassigned + // e.g. in an apoc.cypher.runFirstColumn function call used in createProjection->connectionField + .replace(/REPLACE_ME(?=\w+: \$REPLACE_ME)/g, "projection") + .replace(/\$REPLACE_ME/g, "$projection") + .replace(/REPLACE_ME/g, `${rootNodeVariable.getCypher(env)}`)}`; + }); + } + + const replacedConnectionStrs = connectionStrs.length + ? new Cypher.RawCypher((env: Cypher.Environment) => { + return connectionStrs + .map((connectionStr) => connectionStr.replace(/REPLACE_ME/g, `${rootNodeVariable.getCypher(env)}`)) + .join("\n"); + }) + : undefined; + + const replacedInterfaceStrs = interfaceStrs.length + ? new Cypher.RawCypher((env: Cypher.Environment) => { + return interfaceStrs + .map((interfaceStr) => interfaceStr.replace(/REPLACE_ME/g, `${rootNodeVariable.getCypher(env)}`)) + .join("\n"); + }) + : undefined; + + const unwindCreate = Cypher.concat(unwindQuery, createCypher); + const returnStatement = generateCreateReturnStatementCypher(projectionCypher, context.subscriptionsEnabled); + const projectionWithStr = context.subscriptionsEnabled ? `WITH ${projectionWith.join(", ")}` : ""; + + const createQuery = new Cypher.RawCypher((env) => { + const projectionSubqueryStr = compileCypherIfExists(projectionSubquery, env); + const projectionConnectionStrs = compileCypherIfExists(replacedConnectionStrs, env); + const projectionInterfaceStrs = compileCypherIfExists(replacedInterfaceStrs, env); + + const replacedProjectionSubqueryStrs = projectionSubqueryStr + .replace(/REPLACE_ME(?=\w+: \$REPLACE_ME)/g, "projection") + .replace(/\$REPLACE_ME/g, "$projection") + .replace(/REPLACE_ME/g, `${rootNodeVariable.getCypher(env)}`); + + const cypher = filterTruthy([ + unwindCreate.getCypher(env), + projectionWithStr, + authCalls, + projectionConnectionStrs, + projectionInterfaceStrs, + replacedProjectionSubqueryStrs, + returnStatement.getCypher(env), + ]) + .filter(Boolean) + .join("\n"); + + return [ + cypher, + { + ...replacedProjectionParams, + }, + ]; + }); + const createQueryCypher = createQuery.build("create_"); + const { cypher, params: resolvedCallbacks } = await callbackBucket.resolveCallbacksAndFilterCypher({ + cypher: createQueryCypher.cypher, + }); + + return { + cypher, + params: { + ...createQueryCypher.params, + resolvedCallbacks, + }, + }; +} + +function generateCreateReturnStatementCypher( + projection: Cypher.Expr | undefined, + subscriptionsEnabled: boolean +): Cypher.Expr { + return new Cypher.RawCypher((env: Cypher.Environment) => { + const statements: string[] = []; + + if (projection) { + statements.push(`collect(${projection.getCypher(env)}) AS data`); + } + + if (subscriptionsEnabled) { + statements.push(META_CYPHER_VARIABLE); + } + + if (statements.length === 0) { + statements.push("'Query cannot conclude with CALL'"); + } + + return `RETURN ${statements.join(", ")}`; + }); +} diff --git a/packages/graphql/src/utils/compile-cypher-if-exists.ts b/packages/graphql/src/utils/compile-cypher-if-exists.ts new file mode 100644 index 0000000000..0236ad17c6 --- /dev/null +++ b/packages/graphql/src/utils/compile-cypher-if-exists.ts @@ -0,0 +1,32 @@ +/* + * 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: any, + env: Cypher.Environment, + { prefix = "", suffix = "" }: { prefix?: string; suffix?: string } = {} +): string { + if (!element) return ""; + const cypher = element.getCypher(env); + if (!cypher) return ""; + return `${prefix}${cypher}${suffix}`; +} diff --git a/packages/graphql/tests/integration/issues/2068.int.test.ts b/packages/graphql/tests/integration/issues/2068.int.test.ts index 11a77bdd66..a881764ade 100644 --- a/packages/graphql/tests/integration/issues/2068.int.test.ts +++ b/packages/graphql/tests/integration/issues/2068.int.test.ts @@ -109,7 +109,6 @@ describe("https://github.com/neo4j/graphql/pull/2068", () => { contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark(), { req }), }); - console.log(gqlResult.errors); expect(gqlResult.errors).toBeUndefined(); const users = (gqlResult.data as any)[userType.operations.update][userType.plural] as any[]; diff --git a/packages/graphql/tests/integration/unwind-create/unwind-create.int.test.ts b/packages/graphql/tests/integration/unwind-create/unwind-create.int.test.ts new file mode 100644 index 0000000000..051aa1cd8a --- /dev/null +++ b/packages/graphql/tests/integration/unwind-create/unwind-create.int.test.ts @@ -0,0 +1,811 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Driver } from "neo4j-driver"; +import { int } from "neo4j-driver"; +import { graphql } from "graphql"; +import { generate } from "randomstring"; +import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { generateUniqueType } from "../../utils/graphql-types"; + +describe("unwind-create", () => { + let driver: Driver; + let neo4j: Neo4j; + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + }); + + afterAll(async () => { + await driver.close(); + }); + + test("should create a batch of movies", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + + const typeDefs = ` + type ${Movie} { + id: ID! + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const id2 = generate({ + charset: "alphabetic", + }); + + const query = ` + mutation($id: ID!, $id2: ID!) { + ${Movie.operations.create}(input: [{ id: $id }, {id: $id2 }]) { + ${Movie.plural} { + id + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { id, id2 }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([{ id }, { id: id2 }]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie}) + RETURN m + `, + {} + ); + const records = reFind.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.arrayContaining([ + { m: expect.objectContaining({ properties: { id } }) }, + { m: expect.objectContaining({ properties: { id: id2 } }) }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should create a batch of movies with nested actors", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + + const typeDefs = ` + type ${Actor} { + name: String! + } + type ${Movie} { + id: ID! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const id2 = generate({ + charset: "alphabetic", + }); + + const actor1Name = generate({ + charset: "alphabetic", + }); + + const actor2Name = generate({ + charset: "alphabetic", + }); + + const query = ` + mutation($id: ID!, $id2: ID!, $actor1Name: String!, $actor2Name: String!) { + ${Movie.operations.create}(input: [ + { id: $id, actors: { create: { node: { name: $actor1Name}} } }, + { id: $id2, actors: { create: { node: { name: $actor2Name}} } } + ]) { + ${Movie.plural} { + id + actors { + name + } + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { id, id2, actor1Name, actor2Name }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([ + { id, actors: [{ name: actor1Name }] }, + { id: id2, actors: [{ name: actor2Name }] }, + ]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie})<-[:ACTED_IN]-(a:${Actor}) + RETURN m,a + `, + {} + ); + + const records = reFind.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.arrayContaining([ + { + m: expect.objectContaining({ properties: { id } }), + a: expect.objectContaining({ properties: { name: actor1Name } }), + }, + { + m: expect.objectContaining({ properties: { id: id2 } }), + a: expect.objectContaining({ properties: { name: actor2Name } }), + }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should create a batch of movies with nested actors with nested movies", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + + const typeDefs = ` + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + type ${Movie} { + id: ID! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const id2 = generate({ + charset: "alphabetic", + }); + + const id3 = generate({ + charset: "alphabetic", + }); + + const id4 = generate({ + charset: "alphabetic", + }); + + const actor1Name = generate({ + charset: "alphabetic", + }); + + const actor2Name = generate({ + charset: "alphabetic", + }); + + const query = ` + mutation($id: ID!, $id2: ID!, $id3: ID!, $id4: ID!, $actor1Name: String!, $actor2Name: String!) { + ${Movie.operations.create}(input: [ + { + id: $id, + actors: { + create: { + node: { + name: $actor1Name, + movies: { + create: { node: { id: $id3 } } + } + } + } + } + }, + { + id: $id2, + actors: { + create: { + node: { + name: $actor2Name, + movies: { + create: { node: { id: $id4 } } + } + } + } + } + } + ]) { + ${Movie.plural} { + id + actors { + name + } + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { id, id2, id3, id4, actor1Name, actor2Name }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([ + { id, actors: [{ name: actor1Name }] }, + { id: id2, actors: [{ name: actor2Name }] }, + ]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie})<-[:ACTED_IN]-(a:${Actor}) + RETURN m,a + `, + {} + ); + + const records = reFind.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.arrayContaining([ + { + m: expect.objectContaining({ properties: { id } }), + a: expect.objectContaining({ properties: { name: actor1Name } }), + }, + { + m: expect.objectContaining({ properties: { id: id2 } }), + a: expect.objectContaining({ properties: { name: actor2Name } }), + }, + { + m: expect.objectContaining({ properties: { id: id3 } }), + a: expect.objectContaining({ properties: { name: actor1Name } }), + }, + { + m: expect.objectContaining({ properties: { id: id4 } }), + a: expect.objectContaining({ properties: { name: actor2Name } }), + }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should create a batch of movies with nested actors with edge properties", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + + const typeDefs = ` + type ${Actor} { + name: String! + } + type ${Movie} { + id: ID! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + year: Int + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const id2 = generate({ + charset: "alphabetic", + }); + + const actor1Name = generate({ + charset: "alphabetic", + }); + + const actor2Name = generate({ + charset: "alphabetic", + }); + + const query = ` + mutation($id: ID!, $id2: ID!, $actor1Name: String!, $actor2Name: String!) { + ${Movie.operations.create}(input: [ + { id: $id, actors: { create: { node: { name: $actor1Name }, edge: { year: 2022 } } } }, + { id: $id2, actors: { create: { node: { name: $actor2Name }, edge: { year: 2021 } } } } + ]) { + ${Movie.plural} { + id + actors { + name + } + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { id, id2, actor1Name, actor2Name }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([ + { id, actors: [{ name: actor1Name }] }, + { id: id2, actors: [{ name: actor2Name }] }, + ]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie})<-[r:ACTED_IN]-(a:${Actor}) + RETURN m,r,a + `, + {} + ); + + const records = reFind.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.arrayContaining([ + { + m: expect.objectContaining({ properties: { id } }), + r: expect.objectContaining({ properties: { year: int(2022) } }), + a: expect.objectContaining({ properties: { name: actor1Name } }), + }, + { + m: expect.objectContaining({ properties: { id: id2 } }), + r: expect.objectContaining({ properties: { year: int(2021) } }), + a: expect.objectContaining({ properties: { name: actor2Name } }), + }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should create a batch of movies with nested persons using interface", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + const Modeler = generateUniqueType("Modeler"); + const Person = generateUniqueType("Person"); + + const workedIn = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + interface ${Person} { + name: String + } + + type ${Modeler} implements ${Person} { + name: String + } + + type ${Actor} implements ${Person} { + name: String! + } + + type ${Movie} { + id: ID! + workers: [${Person}!]! @relationship(type: "${workedIn}", direction: IN, properties: "WorkedIn") + } + + interface WorkedIn @relationshipProperties { + year: Int + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const id2 = generate({ + charset: "alphabetic", + }); + + const actorName = generate({ + charset: "alphabetic", + }); + + const modelerName = generate({ + charset: "alphabetic", + }); + + const query = ` + mutation($id: ID!, $id2: ID!, $actorName: String!, $modelerName: String!) { + ${Movie.operations.create}(input: [ + { id: $id, workers: { create: { node: { ${Actor}: { name: $actorName } }, edge: { year: 2022 } } } }, + { id: $id2, workers: { create: { node: { ${Modeler}: { name: $modelerName } }, edge: { year: 2021 } } } } + ]) { + ${Movie.plural} { + id + workers { + name + } + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { id, id2, actorName, modelerName }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([ + { id, workers: [{ name: actorName }] }, + { id: id2, workers: [{ name: modelerName }] }, + ]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie})<-[r:${workedIn}]-(a) + RETURN m,r,a + `, + {} + ); + + const records = reFind.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.arrayContaining([ + { + m: expect.objectContaining({ properties: { id } }), + r: expect.objectContaining({ properties: { year: int(2022) } }), + a: expect.objectContaining({ properties: { name: actorName } }), + }, + { + m: expect.objectContaining({ properties: { id: id2 } }), + r: expect.objectContaining({ properties: { year: int(2021) } }), + a: expect.objectContaining({ properties: { name: modelerName } }), + }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should set properties defined with the directive @alias", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + + const actedIn = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + + type ${Actor} { + name: String! + nickname: String @alias(property: "alternativeName") + } + + type ${Movie} { + id: ID! + name: String @alias(property: "title") + actors: [${Actor}!]! @relationship(type: "${actedIn}", direction: IN, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + year: Int + pay: Int @alias(property: "salary") + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const id = generate({ + charset: "alphabetic", + }); + + const movieName = "Matrix"; + + const id2 = generate({ + charset: "alphabetic", + }); + + const movie2Name = "Matrix 2"; + + const actorName = generate({ + charset: "alphabetic", + }); + + const actorNickname = generate({ + charset: "alphabetic", + }); + + const actorPay = 10200; + + const actor2Name = generate({ + charset: "alphabetic", + }); + + const actor2Nickname = generate({ + charset: "alphabetic", + }); + const actor2Pay = 1232; + + const query = ` + mutation( + $id: ID!, + $movieName: String, + $id2: ID!, + $movie2Name: String, + $actorName: String!, + $actorNickname: String!, + $actorPay: Int, + $actor2Name: String!, + $actor2Nickname: String!, + $actor2Pay: Int + ) { + ${Movie.operations.create}(input: [ + { + id: $id, + name: $movieName, + actors: { + create: { node: { name: $actorName, nickname: $actorNickname }, edge: { year: 2022, pay: $actorPay } } + } + }, + { + id: $id2, + name: $movie2Name, + actors: { + create: { node: { name: $actor2Name, nickname: $actor2Nickname }, edge: { year: 2021, pay: $actor2Pay } } + } + } + ]) { + ${Movie.plural} { + id + name + actors { + name + nickname + } + } + } + } + `; + + try { + const gqlResult = await graphql({ + schema: await neoSchema.getSchema(), + source: query, + variableValues: { + id, + movieName, + id2, + movie2Name, + actorName, + actorNickname, + actorPay, + actor2Name, + actor2Nickname, + actor2Pay, + }, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( + expect.arrayContaining([ + { id, name: movieName, actors: [{ name: actorName, nickname: actorNickname }] }, + { id: id2, name: movie2Name, actors: [{ name: actor2Name, nickname: actor2Nickname }] }, + ]) + ); + + const reFind = await session.run( + ` + MATCH (m:${Movie})<-[r:${actedIn}]-(a) + RETURN m,r,a + `, + {} + ); + + const records = reFind.records.map((record) => record.toObject()); + + expect(records).toEqual( + expect.arrayContaining([ + { + m: expect.objectContaining({ properties: { id, title: movieName } }), + r: expect.objectContaining({ properties: { year: int(2022), salary: int(actorPay) } }), + a: expect.objectContaining({ properties: { name: actorName, alternativeName: actorNickname } }), + }, + { + m: expect.objectContaining({ properties: { id: id2, title: movie2Name } }), + r: expect.objectContaining({ properties: { year: int(2021), salary: int(actor2Pay) } }), + a: expect.objectContaining({ + properties: { name: actor2Name, alternativeName: actor2Nickname }, + }), + }, + ]) + ); + } finally { + await session.close(); + } + }); + + test("should a batch of actors with nested movies and resolve actorsConnection", async () => { + const session = await neo4j.getSession(); + + const Movie = generateUniqueType("Movie"); + const Actor = generateUniqueType("Actor"); + + const typeDefs = ` + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getSchema(); + + const movieTitle = generate({ charset: "alphabetic" }); + const movie2Title = generate({ charset: "alphabetic" }); + const actorName = generate({ charset: "alphabetic" }); + const actor2Name = generate({ charset: "alphabetic" }); + + const query = ` + mutation ($movieTitle: String!, $actorName: String!, $movie2Title: String!, $actor2Name: String!) { + ${Actor.operations.create}( + input: [ + { + name: $actorName + movies: { create: { node: { title: $movieTitle } } } + }, + { + name: $actor2Name + movies: { create: { node: { title: $movie2Title } } } + }, + + ]) { + ${Actor.plural} { + name + movies { + title + actorsConnection(where: { node: { name: $actor2Name } }) { + totalCount + edges { + node { + name + } + } + } + } + } + } + } + `; + try { + const result = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValuesWithBookmarks(session.lastBookmark()), + variableValues: { movieTitle, actorName, movie2Title, actor2Name }, + }); + + expect(result.errors).toBeFalsy(); + expect(result.data?.[Actor.operations.create]).toEqual({ + [Actor.plural]: expect.arrayContaining([ + { + name: actorName, + movies: [ + { + title: movieTitle, + actorsConnection: { + totalCount: 0, + edges: [], + }, + }, + ], + }, + { + name: actor2Name, + movies: [ + { + title: movie2Title, + actorsConnection: { + totalCount: 1, + edges: [ + { + node: { + name: actor2Name, + }, + }, + ], + }, + }, + ], + }, + ]), + }); + } finally { + await session.close(); + } + }); +}); diff --git a/packages/graphql/tests/performance/graphql/batch-create/batch-create-interface.graphql b/packages/graphql/tests/performance/graphql/batch-create/batch-create-interface.graphql new file mode 100644 index 0000000000..e50b295acf --- /dev/null +++ b/packages/graphql/tests/performance/graphql/batch-create/batch-create-interface.graphql @@ -0,0 +1,310 @@ +mutation BatchCreateInterface { + createPeople( + input: [ + { name: "s0", born: 0, likes: { Person: { create: [{ node: { name: "a0", born: 10000 } }] } } } + { name: "s1", born: 1, likes: { Person: { create: [{ node: { name: "a1", born: 10001 } }] } } } + { name: "s2", born: 2, likes: { Person: { create: [{ node: { name: "a2", born: 10002 } }] } } } + { name: "s3", born: 3, likes: { Person: { create: [{ node: { name: "a3", born: 10003 } }] } } } + { name: "s4", born: 4, likes: { Person: { create: [{ node: { name: "a4", born: 10004 } }] } } } + { name: "s5", born: 5, likes: { Person: { create: [{ node: { name: "a5", born: 10005 } }] } } } + { name: "s6", born: 6, likes: { Person: { create: [{ node: { name: "a6", born: 10006 } }] } } } + { name: "s7", born: 7, likes: { Person: { create: [{ node: { name: "a7", born: 10007 } }] } } } + { name: "s8", born: 8, likes: { Person: { create: [{ node: { name: "a8", born: 10008 } }] } } } + { name: "s9", born: 9, likes: { Person: { create: [{ node: { name: "a9", born: 10009 } }] } } } + { name: "s10", born: 10, likes: { Person: { create: [{ node: { name: "a10", born: 10010 } }] } } } + { name: "s11", born: 11, likes: { Person: { create: [{ node: { name: "a11", born: 10011 } }] } } } + { name: "s12", born: 12, likes: { Person: { create: [{ node: { name: "a12", born: 10012 } }] } } } + { name: "s13", born: 13, likes: { Person: { create: [{ node: { name: "a13", born: 10013 } }] } } } + { name: "s14", born: 14, likes: { Person: { create: [{ node: { name: "a14", born: 10014 } }] } } } + { name: "s15", born: 15, likes: { Person: { create: [{ node: { name: "a15", born: 10015 } }] } } } + { name: "s16", born: 16, likes: { Person: { create: [{ node: { name: "a16", born: 10016 } }] } } } + { name: "s17", born: 17, likes: { Person: { create: [{ node: { name: "a17", born: 10017 } }] } } } + { name: "s18", born: 18, likes: { Person: { create: [{ node: { name: "a18", born: 10018 } }] } } } + { name: "s19", born: 19, likes: { Person: { create: [{ node: { name: "a19", born: 10019 } }] } } } + { name: "s20", born: 20, likes: { Person: { create: [{ node: { name: "a20", born: 10020 } }] } } } + { name: "s21", born: 21, likes: { Person: { create: [{ node: { name: "a21", born: 10021 } }] } } } + { name: "s22", born: 22, likes: { Person: { create: [{ node: { name: "a22", born: 10022 } }] } } } + { name: "s23", born: 23, likes: { Person: { create: [{ node: { name: "a23", born: 10023 } }] } } } + { name: "s24", born: 24, likes: { Person: { create: [{ node: { name: "a24", born: 10024 } }] } } } + { name: "s25", born: 25, likes: { Person: { create: [{ node: { name: "a25", born: 10025 } }] } } } + { name: "s26", born: 26, likes: { Person: { create: [{ node: { name: "a26", born: 10026 } }] } } } + { name: "s27", born: 27, likes: { Person: { create: [{ node: { name: "a27", born: 10027 } }] } } } + { name: "s28", born: 28, likes: { Person: { create: [{ node: { name: "a28", born: 10028 } }] } } } + { name: "s29", born: 29, likes: { Person: { create: [{ node: { name: "a29", born: 10029 } }] } } } + { name: "s30", born: 30, likes: { Person: { create: [{ node: { name: "a30", born: 10030 } }] } } } + { name: "s31", born: 31, likes: { Person: { create: [{ node: { name: "a31", born: 10031 } }] } } } + { name: "s32", born: 32, likes: { Person: { create: [{ node: { name: "a32", born: 10032 } }] } } } + { name: "s33", born: 33, likes: { Person: { create: [{ node: { name: "a33", born: 10033 } }] } } } + { name: "s34", born: 34, likes: { Person: { create: [{ node: { name: "a34", born: 10034 } }] } } } + { name: "s35", born: 35, likes: { Person: { create: [{ node: { name: "a35", born: 10035 } }] } } } + { name: "s36", born: 36, likes: { Person: { create: [{ node: { name: "a36", born: 10036 } }] } } } + { name: "s37", born: 37, likes: { Person: { create: [{ node: { name: "a37", born: 10037 } }] } } } + { name: "s38", born: 38, likes: { Person: { create: [{ node: { name: "a38", born: 10038 } }] } } } + { name: "s39", born: 39, likes: { Person: { create: [{ node: { name: "a39", born: 10039 } }] } } } + { name: "s40", born: 40, likes: { Person: { create: [{ node: { name: "a40", born: 10040 } }] } } } + { name: "s41", born: 41, likes: { Person: { create: [{ node: { name: "a41", born: 10041 } }] } } } + { name: "s42", born: 42, likes: { Person: { create: [{ node: { name: "a42", born: 10042 } }] } } } + { name: "s43", born: 43, likes: { Person: { create: [{ node: { name: "a43", born: 10043 } }] } } } + { name: "s44", born: 44, likes: { Person: { create: [{ node: { name: "a44", born: 10044 } }] } } } + { name: "s45", born: 45, likes: { Person: { create: [{ node: { name: "a45", born: 10045 } }] } } } + { name: "s46", born: 46, likes: { Person: { create: [{ node: { name: "a46", born: 10046 } }] } } } + { name: "s47", born: 47, likes: { Person: { create: [{ node: { name: "a47", born: 10047 } }] } } } + { name: "s48", born: 48, likes: { Person: { create: [{ node: { name: "a48", born: 10048 } }] } } } + { name: "s49", born: 49, likes: { Person: { create: [{ node: { name: "a49", born: 10049 } }] } } } + { name: "s50", born: 50, likes: { Person: { create: [{ node: { name: "a50", born: 10050 } }] } } } + { name: "s51", born: 51, likes: { Person: { create: [{ node: { name: "a51", born: 10051 } }] } } } + { name: "s52", born: 52, likes: { Person: { create: [{ node: { name: "a52", born: 10052 } }] } } } + { name: "s53", born: 53, likes: { Person: { create: [{ node: { name: "a53", born: 10053 } }] } } } + { name: "s54", born: 54, likes: { Person: { create: [{ node: { name: "a54", born: 10054 } }] } } } + { name: "s55", born: 55, likes: { Person: { create: [{ node: { name: "a55", born: 10055 } }] } } } + { name: "s56", born: 56, likes: { Person: { create: [{ node: { name: "a56", born: 10056 } }] } } } + { name: "s57", born: 57, likes: { Person: { create: [{ node: { name: "a57", born: 10057 } }] } } } + { name: "s58", born: 58, likes: { Person: { create: [{ node: { name: "a58", born: 10058 } }] } } } + { name: "s59", born: 59, likes: { Person: { create: [{ node: { name: "a59", born: 10059 } }] } } } + { name: "s60", born: 60, likes: { Person: { create: [{ node: { name: "a60", born: 10060 } }] } } } + { name: "s61", born: 61, likes: { Person: { create: [{ node: { name: "a61", born: 10061 } }] } } } + { name: "s62", born: 62, likes: { Person: { create: [{ node: { name: "a62", born: 10062 } }] } } } + { name: "s63", born: 63, likes: { Person: { create: [{ node: { name: "a63", born: 10063 } }] } } } + { name: "s64", born: 64, likes: { Person: { create: [{ node: { name: "a64", born: 10064 } }] } } } + { name: "s65", born: 65, likes: { Person: { create: [{ node: { name: "a65", born: 10065 } }] } } } + { name: "s66", born: 66, likes: { Person: { create: [{ node: { name: "a66", born: 10066 } }] } } } + { name: "s67", born: 67, likes: { Person: { create: [{ node: { name: "a67", born: 10067 } }] } } } + { name: "s68", born: 68, likes: { Person: { create: [{ node: { name: "a68", born: 10068 } }] } } } + { name: "s69", born: 69, likes: { Person: { create: [{ node: { name: "a69", born: 10069 } }] } } } + { name: "s70", born: 70, likes: { Person: { create: [{ node: { name: "a70", born: 10070 } }] } } } + { name: "s71", born: 71, likes: { Person: { create: [{ node: { name: "a71", born: 10071 } }] } } } + { name: "s72", born: 72, likes: { Person: { create: [{ node: { name: "a72", born: 10072 } }] } } } + { name: "s73", born: 73, likes: { Person: { create: [{ node: { name: "a73", born: 10073 } }] } } } + { name: "s74", born: 74, likes: { Person: { create: [{ node: { name: "a74", born: 10074 } }] } } } + { name: "s75", born: 75, likes: { Person: { create: [{ node: { name: "a75", born: 10075 } }] } } } + { name: "s76", born: 76, likes: { Person: { create: [{ node: { name: "a76", born: 10076 } }] } } } + { name: "s77", born: 77, likes: { Person: { create: [{ node: { name: "a77", born: 10077 } }] } } } + { name: "s78", born: 78, likes: { Person: { create: [{ node: { name: "a78", born: 10078 } }] } } } + { name: "s79", born: 79, likes: { Person: { create: [{ node: { name: "a79", born: 10079 } }] } } } + { name: "s80", born: 80, likes: { Person: { create: [{ node: { name: "a80", born: 10080 } }] } } } + { name: "s81", born: 81, likes: { Person: { create: [{ node: { name: "a81", born: 10081 } }] } } } + { name: "s82", born: 82, likes: { Person: { create: [{ node: { name: "a82", born: 10082 } }] } } } + { name: "s83", born: 83, likes: { Person: { create: [{ node: { name: "a83", born: 10083 } }] } } } + { name: "s84", born: 84, likes: { Person: { create: [{ node: { name: "a84", born: 10084 } }] } } } + { name: "s85", born: 85, likes: { Person: { create: [{ node: { name: "a85", born: 10085 } }] } } } + { name: "s86", born: 86, likes: { Person: { create: [{ node: { name: "a86", born: 10086 } }] } } } + { name: "s87", born: 87, likes: { Person: { create: [{ node: { name: "a87", born: 10087 } }] } } } + { name: "s88", born: 88, likes: { Person: { create: [{ node: { name: "a88", born: 10088 } }] } } } + { name: "s89", born: 89, likes: { Person: { create: [{ node: { name: "a89", born: 10089 } }] } } } + { name: "s90", born: 90, likes: { Person: { create: [{ node: { name: "a90", born: 10090 } }] } } } + { name: "s91", born: 91, likes: { Person: { create: [{ node: { name: "a91", born: 10091 } }] } } } + { name: "s92", born: 92, likes: { Person: { create: [{ node: { name: "a92", born: 10092 } }] } } } + { name: "s93", born: 93, likes: { Person: { create: [{ node: { name: "a93", born: 10093 } }] } } } + { name: "s94", born: 94, likes: { Person: { create: [{ node: { name: "a94", born: 10094 } }] } } } + { name: "s95", born: 95, likes: { Person: { create: [{ node: { name: "a95", born: 10095 } }] } } } + { name: "s96", born: 96, likes: { Person: { create: [{ node: { name: "a96", born: 10096 } }] } } } + { name: "s97", born: 97, likes: { Person: { create: [{ node: { name: "a97", born: 10097 } }] } } } + { name: "s98", born: 98, likes: { Person: { create: [{ node: { name: "a98", born: 10098 } }] } } } + { name: "s99", born: 99, likes: { Person: { create: [{ node: { name: "a99", born: 10099 } }] } } } + { name: "s100", born: 100, likes: { Person: { create: [{ node: { name: "a100", born: 10100 } }] } } } + { name: "s101", born: 101, likes: { Person: { create: [{ node: { name: "a101", born: 10101 } }] } } } + { name: "s102", born: 102, likes: { Person: { create: [{ node: { name: "a102", born: 10102 } }] } } } + { name: "s103", born: 103, likes: { Person: { create: [{ node: { name: "a103", born: 10103 } }] } } } + { name: "s104", born: 104, likes: { Person: { create: [{ node: { name: "a104", born: 10104 } }] } } } + { name: "s105", born: 105, likes: { Person: { create: [{ node: { name: "a105", born: 10105 } }] } } } + { name: "s106", born: 106, likes: { Person: { create: [{ node: { name: "a106", born: 10106 } }] } } } + { name: "s107", born: 107, likes: { Person: { create: [{ node: { name: "a107", born: 10107 } }] } } } + { name: "s108", born: 108, likes: { Person: { create: [{ node: { name: "a108", born: 10108 } }] } } } + { name: "s109", born: 109, likes: { Person: { create: [{ node: { name: "a109", born: 10109 } }] } } } + { name: "s110", born: 110, likes: { Person: { create: [{ node: { name: "a110", born: 10110 } }] } } } + { name: "s111", born: 111, likes: { Person: { create: [{ node: { name: "a111", born: 10111 } }] } } } + { name: "s112", born: 112, likes: { Person: { create: [{ node: { name: "a112", born: 10112 } }] } } } + { name: "s113", born: 113, likes: { Person: { create: [{ node: { name: "a113", born: 10113 } }] } } } + { name: "s114", born: 114, likes: { Person: { create: [{ node: { name: "a114", born: 10114 } }] } } } + { name: "s115", born: 115, likes: { Person: { create: [{ node: { name: "a115", born: 10115 } }] } } } + { name: "s116", born: 116, likes: { Person: { create: [{ node: { name: "a116", born: 10116 } }] } } } + { name: "s117", born: 117, likes: { Person: { create: [{ node: { name: "a117", born: 10117 } }] } } } + { name: "s118", born: 118, likes: { Person: { create: [{ node: { name: "a118", born: 10118 } }] } } } + { name: "s119", born: 119, likes: { Person: { create: [{ node: { name: "a119", born: 10119 } }] } } } + { name: "s120", born: 120, likes: { Person: { create: [{ node: { name: "a120", born: 10120 } }] } } } + { name: "s121", born: 121, likes: { Person: { create: [{ node: { name: "a121", born: 10121 } }] } } } + { name: "s122", born: 122, likes: { Person: { create: [{ node: { name: "a122", born: 10122 } }] } } } + { name: "s123", born: 123, likes: { Person: { create: [{ node: { name: "a123", born: 10123 } }] } } } + { name: "s124", born: 124, likes: { Person: { create: [{ node: { name: "a124", born: 10124 } }] } } } + { name: "s125", born: 125, likes: { Person: { create: [{ node: { name: "a125", born: 10125 } }] } } } + { name: "s126", born: 126, likes: { Person: { create: [{ node: { name: "a126", born: 10126 } }] } } } + { name: "s127", born: 127, likes: { Person: { create: [{ node: { name: "a127", born: 10127 } }] } } } + { name: "s128", born: 128, likes: { Person: { create: [{ node: { name: "a128", born: 10128 } }] } } } + { name: "s129", born: 129, likes: { Person: { create: [{ node: { name: "a129", born: 10129 } }] } } } + { name: "s130", born: 130, likes: { Person: { create: [{ node: { name: "a130", born: 10130 } }] } } } + { name: "s131", born: 131, likes: { Person: { create: [{ node: { name: "a131", born: 10131 } }] } } } + { name: "s132", born: 132, likes: { Person: { create: [{ node: { name: "a132", born: 10132 } }] } } } + { name: "s133", born: 133, likes: { Person: { create: [{ node: { name: "a133", born: 10133 } }] } } } + { name: "s134", born: 134, likes: { Person: { create: [{ node: { name: "a134", born: 10134 } }] } } } + { name: "s135", born: 135, likes: { Person: { create: [{ node: { name: "a135", born: 10135 } }] } } } + { name: "s136", born: 136, likes: { Person: { create: [{ node: { name: "a136", born: 10136 } }] } } } + { name: "s137", born: 137, likes: { Person: { create: [{ node: { name: "a137", born: 10137 } }] } } } + { name: "s138", born: 138, likes: { Person: { create: [{ node: { name: "a138", born: 10138 } }] } } } + { name: "s139", born: 139, likes: { Person: { create: [{ node: { name: "a139", born: 10139 } }] } } } + { name: "s140", born: 140, likes: { Person: { create: [{ node: { name: "a140", born: 10140 } }] } } } + { name: "s141", born: 141, likes: { Person: { create: [{ node: { name: "a141", born: 10141 } }] } } } + { name: "s142", born: 142, likes: { Person: { create: [{ node: { name: "a142", born: 10142 } }] } } } + { name: "s143", born: 143, likes: { Person: { create: [{ node: { name: "a143", born: 10143 } }] } } } + { name: "s144", born: 144, likes: { Person: { create: [{ node: { name: "a144", born: 10144 } }] } } } + { name: "s145", born: 145, likes: { Person: { create: [{ node: { name: "a145", born: 10145 } }] } } } + { name: "s146", born: 146, likes: { Person: { create: [{ node: { name: "a146", born: 10146 } }] } } } + { name: "s147", born: 147, likes: { Person: { create: [{ node: { name: "a147", born: 10147 } }] } } } + { name: "s148", born: 148, likes: { Person: { create: [{ node: { name: "a148", born: 10148 } }] } } } + { name: "s149", born: 149, likes: { Person: { create: [{ node: { name: "a149", born: 10149 } }] } } } + { name: "s150", born: 150, likes: { Person: { create: [{ node: { name: "a150", born: 10150 } }] } } } + { name: "s151", born: 151, likes: { Person: { create: [{ node: { name: "a151", born: 10151 } }] } } } + { name: "s152", born: 152, likes: { Person: { create: [{ node: { name: "a152", born: 10152 } }] } } } + { name: "s153", born: 153, likes: { Person: { create: [{ node: { name: "a153", born: 10153 } }] } } } + { name: "s154", born: 154, likes: { Person: { create: [{ node: { name: "a154", born: 10154 } }] } } } + { name: "s155", born: 155, likes: { Person: { create: [{ node: { name: "a155", born: 10155 } }] } } } + { name: "s156", born: 156, likes: { Person: { create: [{ node: { name: "a156", born: 10156 } }] } } } + { name: "s157", born: 157, likes: { Person: { create: [{ node: { name: "a157", born: 10157 } }] } } } + { name: "s158", born: 158, likes: { Person: { create: [{ node: { name: "a158", born: 10158 } }] } } } + { name: "s159", born: 159, likes: { Person: { create: [{ node: { name: "a159", born: 10159 } }] } } } + { name: "s160", born: 160, likes: { Person: { create: [{ node: { name: "a160", born: 10160 } }] } } } + { name: "s161", born: 161, likes: { Person: { create: [{ node: { name: "a161", born: 10161 } }] } } } + { name: "s162", born: 162, likes: { Person: { create: [{ node: { name: "a162", born: 10162 } }] } } } + { name: "s163", born: 163, likes: { Person: { create: [{ node: { name: "a163", born: 10163 } }] } } } + { name: "s164", born: 164, likes: { Person: { create: [{ node: { name: "a164", born: 10164 } }] } } } + { name: "s165", born: 165, likes: { Person: { create: [{ node: { name: "a165", born: 10165 } }] } } } + { name: "s166", born: 166, likes: { Person: { create: [{ node: { name: "a166", born: 10166 } }] } } } + { name: "s167", born: 167, likes: { Person: { create: [{ node: { name: "a167", born: 10167 } }] } } } + { name: "s168", born: 168, likes: { Person: { create: [{ node: { name: "a168", born: 10168 } }] } } } + { name: "s169", born: 169, likes: { Person: { create: [{ node: { name: "a169", born: 10169 } }] } } } + { name: "s170", born: 170, likes: { Person: { create: [{ node: { name: "a170", born: 10170 } }] } } } + { name: "s171", born: 171, likes: { Person: { create: [{ node: { name: "a171", born: 10171 } }] } } } + { name: "s172", born: 172, likes: { Person: { create: [{ node: { name: "a172", born: 10172 } }] } } } + { name: "s173", born: 173, likes: { Person: { create: [{ node: { name: "a173", born: 10173 } }] } } } + { name: "s174", born: 174, likes: { Person: { create: [{ node: { name: "a174", born: 10174 } }] } } } + { name: "s175", born: 175, likes: { Person: { create: [{ node: { name: "a175", born: 10175 } }] } } } + { name: "s176", born: 176, likes: { Person: { create: [{ node: { name: "a176", born: 10176 } }] } } } + { name: "s177", born: 177, likes: { Person: { create: [{ node: { name: "a177", born: 10177 } }] } } } + { name: "s178", born: 178, likes: { Person: { create: [{ node: { name: "a178", born: 10178 } }] } } } + { name: "s179", born: 179, likes: { Person: { create: [{ node: { name: "a179", born: 10179 } }] } } } + { name: "s180", born: 180, likes: { Person: { create: [{ node: { name: "a180", born: 10180 } }] } } } + { name: "s181", born: 181, likes: { Person: { create: [{ node: { name: "a181", born: 10181 } }] } } } + { name: "s182", born: 182, likes: { Person: { create: [{ node: { name: "a182", born: 10182 } }] } } } + { name: "s183", born: 183, likes: { Person: { create: [{ node: { name: "a183", born: 10183 } }] } } } + { name: "s184", born: 184, likes: { Person: { create: [{ node: { name: "a184", born: 10184 } }] } } } + { name: "s185", born: 185, likes: { Person: { create: [{ node: { name: "a185", born: 10185 } }] } } } + { name: "s186", born: 186, likes: { Person: { create: [{ node: { name: "a186", born: 10186 } }] } } } + { name: "s187", born: 187, likes: { Person: { create: [{ node: { name: "a187", born: 10187 } }] } } } + { name: "s188", born: 188, likes: { Person: { create: [{ node: { name: "a188", born: 10188 } }] } } } + { name: "s189", born: 189, likes: { Person: { create: [{ node: { name: "a189", born: 10189 } }] } } } + { name: "s190", born: 190, likes: { Person: { create: [{ node: { name: "a190", born: 10190 } }] } } } + { name: "s191", born: 191, likes: { Person: { create: [{ node: { name: "a191", born: 10191 } }] } } } + { name: "s192", born: 192, likes: { Person: { create: [{ node: { name: "a192", born: 10192 } }] } } } + { name: "s193", born: 193, likes: { Person: { create: [{ node: { name: "a193", born: 10193 } }] } } } + { name: "s194", born: 194, likes: { Person: { create: [{ node: { name: "a194", born: 10194 } }] } } } + { name: "s195", born: 195, likes: { Person: { create: [{ node: { name: "a195", born: 10195 } }] } } } + { name: "s196", born: 196, likes: { Person: { create: [{ node: { name: "a196", born: 10196 } }] } } } + { name: "s197", born: 197, likes: { Person: { create: [{ node: { name: "a197", born: 10197 } }] } } } + { name: "s198", born: 198, likes: { Person: { create: [{ node: { name: "a198", born: 10198 } }] } } } + { name: "s199", born: 199, likes: { Person: { create: [{ node: { name: "a199", born: 10199 } }] } } } + { name: "s200", born: 200, likes: { Person: { create: [{ node: { name: "a200", born: 10200 } }] } } } + { name: "s201", born: 201, likes: { Person: { create: [{ node: { name: "a201", born: 10201 } }] } } } + { name: "s202", born: 202, likes: { Person: { create: [{ node: { name: "a202", born: 10202 } }] } } } + { name: "s203", born: 203, likes: { Person: { create: [{ node: { name: "a203", born: 10203 } }] } } } + { name: "s204", born: 204, likes: { Person: { create: [{ node: { name: "a204", born: 10204 } }] } } } + { name: "s205", born: 205, likes: { Person: { create: [{ node: { name: "a205", born: 10205 } }] } } } + { name: "s206", born: 206, likes: { Person: { create: [{ node: { name: "a206", born: 10206 } }] } } } + { name: "s207", born: 207, likes: { Person: { create: [{ node: { name: "a207", born: 10207 } }] } } } + { name: "s208", born: 208, likes: { Person: { create: [{ node: { name: "a208", born: 10208 } }] } } } + { name: "s209", born: 209, likes: { Person: { create: [{ node: { name: "a209", born: 10209 } }] } } } + { name: "s210", born: 210, likes: { Person: { create: [{ node: { name: "a210", born: 10210 } }] } } } + { name: "s211", born: 211, likes: { Person: { create: [{ node: { name: "a211", born: 10211 } }] } } } + { name: "s212", born: 212, likes: { Person: { create: [{ node: { name: "a212", born: 10212 } }] } } } + { name: "s213", born: 213, likes: { Person: { create: [{ node: { name: "a213", born: 10213 } }] } } } + { name: "s214", born: 214, likes: { Person: { create: [{ node: { name: "a214", born: 10214 } }] } } } + { name: "s215", born: 215, likes: { Person: { create: [{ node: { name: "a215", born: 10215 } }] } } } + { name: "s216", born: 216, likes: { Person: { create: [{ node: { name: "a216", born: 10216 } }] } } } + { name: "s217", born: 217, likes: { Person: { create: [{ node: { name: "a217", born: 10217 } }] } } } + { name: "s218", born: 218, likes: { Person: { create: [{ node: { name: "a218", born: 10218 } }] } } } + { name: "s219", born: 219, likes: { Person: { create: [{ node: { name: "a219", born: 10219 } }] } } } + { name: "s220", born: 220, likes: { Person: { create: [{ node: { name: "a220", born: 10220 } }] } } } + { name: "s221", born: 221, likes: { Person: { create: [{ node: { name: "a221", born: 10221 } }] } } } + { name: "s222", born: 222, likes: { Person: { create: [{ node: { name: "a222", born: 10222 } }] } } } + { name: "s223", born: 223, likes: { Person: { create: [{ node: { name: "a223", born: 10223 } }] } } } + { name: "s224", born: 224, likes: { Person: { create: [{ node: { name: "a224", born: 10224 } }] } } } + { name: "s225", born: 225, likes: { Person: { create: [{ node: { name: "a225", born: 10225 } }] } } } + { name: "s226", born: 226, likes: { Person: { create: [{ node: { name: "a226", born: 10226 } }] } } } + { name: "s227", born: 227, likes: { Person: { create: [{ node: { name: "a227", born: 10227 } }] } } } + { name: "s228", born: 228, likes: { Person: { create: [{ node: { name: "a228", born: 10228 } }] } } } + { name: "s229", born: 229, likes: { Person: { create: [{ node: { name: "a229", born: 10229 } }] } } } + { name: "s230", born: 230, likes: { Person: { create: [{ node: { name: "a230", born: 10230 } }] } } } + { name: "s231", born: 231, likes: { Person: { create: [{ node: { name: "a231", born: 10231 } }] } } } + { name: "s232", born: 232, likes: { Person: { create: [{ node: { name: "a232", born: 10232 } }] } } } + { name: "s233", born: 233, likes: { Person: { create: [{ node: { name: "a233", born: 10233 } }] } } } + { name: "s234", born: 234, likes: { Person: { create: [{ node: { name: "a234", born: 10234 } }] } } } + { name: "s235", born: 235, likes: { Person: { create: [{ node: { name: "a235", born: 10235 } }] } } } + { name: "s236", born: 236, likes: { Person: { create: [{ node: { name: "a236", born: 10236 } }] } } } + { name: "s237", born: 237, likes: { Person: { create: [{ node: { name: "a237", born: 10237 } }] } } } + { name: "s238", born: 238, likes: { Person: { create: [{ node: { name: "a238", born: 10238 } }] } } } + { name: "s239", born: 239, likes: { Person: { create: [{ node: { name: "a239", born: 10239 } }] } } } + { name: "s240", born: 240, likes: { Person: { create: [{ node: { name: "a240", born: 10240 } }] } } } + { name: "s241", born: 241, likes: { Person: { create: [{ node: { name: "a241", born: 10241 } }] } } } + { name: "s242", born: 242, likes: { Person: { create: [{ node: { name: "a242", born: 10242 } }] } } } + { name: "s243", born: 243, likes: { Person: { create: [{ node: { name: "a243", born: 10243 } }] } } } + { name: "s244", born: 244, likes: { Person: { create: [{ node: { name: "a244", born: 10244 } }] } } } + { name: "s245", born: 245, likes: { Person: { create: [{ node: { name: "a245", born: 10245 } }] } } } + { name: "s246", born: 246, likes: { Person: { create: [{ node: { name: "a246", born: 10246 } }] } } } + { name: "s247", born: 247, likes: { Person: { create: [{ node: { name: "a247", born: 10247 } }] } } } + { name: "s248", born: 248, likes: { Person: { create: [{ node: { name: "a248", born: 10248 } }] } } } + { name: "s249", born: 249, likes: { Person: { create: [{ node: { name: "a249", born: 10249 } }] } } } + { name: "s250", born: 250, likes: { Person: { create: [{ node: { name: "a250", born: 10250 } }] } } } + { name: "s251", born: 251, likes: { Person: { create: [{ node: { name: "a251", born: 10251 } }] } } } + { name: "s252", born: 252, likes: { Person: { create: [{ node: { name: "a252", born: 10252 } }] } } } + { name: "s253", born: 253, likes: { Person: { create: [{ node: { name: "a253", born: 10253 } }] } } } + { name: "s254", born: 254, likes: { Person: { create: [{ node: { name: "a254", born: 10254 } }] } } } + { name: "s255", born: 255, likes: { Person: { create: [{ node: { name: "a255", born: 10255 } }] } } } + { name: "s256", born: 256, likes: { Person: { create: [{ node: { name: "a256", born: 10256 } }] } } } + { name: "s257", born: 257, likes: { Person: { create: [{ node: { name: "a257", born: 10257 } }] } } } + { name: "s258", born: 258, likes: { Person: { create: [{ node: { name: "a258", born: 10258 } }] } } } + { name: "s259", born: 259, likes: { Person: { create: [{ node: { name: "a259", born: 10259 } }] } } } + { name: "s260", born: 260, likes: { Person: { create: [{ node: { name: "a260", born: 10260 } }] } } } + { name: "s261", born: 261, likes: { Person: { create: [{ node: { name: "a261", born: 10261 } }] } } } + { name: "s262", born: 262, likes: { Person: { create: [{ node: { name: "a262", born: 10262 } }] } } } + { name: "s263", born: 263, likes: { Person: { create: [{ node: { name: "a263", born: 10263 } }] } } } + { name: "s264", born: 264, likes: { Person: { create: [{ node: { name: "a264", born: 10264 } }] } } } + { name: "s265", born: 265, likes: { Person: { create: [{ node: { name: "a265", born: 10265 } }] } } } + { name: "s266", born: 266, likes: { Person: { create: [{ node: { name: "a266", born: 10266 } }] } } } + { name: "s267", born: 267, likes: { Person: { create: [{ node: { name: "a267", born: 10267 } }] } } } + { name: "s268", born: 268, likes: { Person: { create: [{ node: { name: "a268", born: 10268 } }] } } } + { name: "s269", born: 269, likes: { Person: { create: [{ node: { name: "a269", born: 10269 } }] } } } + { name: "s270", born: 270, likes: { Person: { create: [{ node: { name: "a270", born: 10270 } }] } } } + { name: "s271", born: 271, likes: { Person: { create: [{ node: { name: "a271", born: 10271 } }] } } } + { name: "s272", born: 272, likes: { Person: { create: [{ node: { name: "a272", born: 10272 } }] } } } + { name: "s273", born: 273, likes: { Person: { create: [{ node: { name: "a273", born: 10273 } }] } } } + { name: "s274", born: 274, likes: { Person: { create: [{ node: { name: "a274", born: 10274 } }] } } } + { name: "s275", born: 275, likes: { Person: { create: [{ node: { name: "a275", born: 10275 } }] } } } + { name: "s276", born: 276, likes: { Person: { create: [{ node: { name: "a276", born: 10276 } }] } } } + { name: "s277", born: 277, likes: { Person: { create: [{ node: { name: "a277", born: 10277 } }] } } } + { name: "s278", born: 278, likes: { Person: { create: [{ node: { name: "a278", born: 10278 } }] } } } + { name: "s279", born: 279, likes: { Person: { create: [{ node: { name: "a279", born: 10279 } }] } } } + { name: "s280", born: 280, likes: { Person: { create: [{ node: { name: "a280", born: 10280 } }] } } } + { name: "s281", born: 281, likes: { Person: { create: [{ node: { name: "a281", born: 10281 } }] } } } + { name: "s282", born: 282, likes: { Person: { create: [{ node: { name: "a282", born: 10282 } }] } } } + { name: "s283", born: 283, likes: { Person: { create: [{ node: { name: "a283", born: 10283 } }] } } } + { name: "s284", born: 284, likes: { Person: { create: [{ node: { name: "a284", born: 10284 } }] } } } + { name: "s285", born: 285, likes: { Person: { create: [{ node: { name: "a285", born: 10285 } }] } } } + { name: "s286", born: 286, likes: { Person: { create: [{ node: { name: "a286", born: 10286 } }] } } } + { name: "s287", born: 287, likes: { Person: { create: [{ node: { name: "a287", born: 10287 } }] } } } + { name: "s288", born: 288, likes: { Person: { create: [{ node: { name: "a288", born: 10288 } }] } } } + { name: "s289", born: 289, likes: { Person: { create: [{ node: { name: "a289", born: 10289 } }] } } } + { name: "s290", born: 290, likes: { Person: { create: [{ node: { name: "a290", born: 10290 } }] } } } + { name: "s291", born: 291, likes: { Person: { create: [{ node: { name: "a291", born: 10291 } }] } } } + { name: "s292", born: 292, likes: { Person: { create: [{ node: { name: "a292", born: 10292 } }] } } } + { name: "s293", born: 293, likes: { Person: { create: [{ node: { name: "a293", born: 10293 } }] } } } + { name: "s294", born: 294, likes: { Person: { create: [{ node: { name: "a294", born: 10294 } }] } } } + { name: "s295", born: 295, likes: { Person: { create: [{ node: { name: "a295", born: 10295 } }] } } } + { name: "s296", born: 296, likes: { Person: { create: [{ node: { name: "a296", born: 10296 } }] } } } + { name: "s297", born: 297, likes: { Person: { create: [{ node: { name: "a297", born: 10297 } }] } } } + { name: "s298", born: 298, likes: { Person: { create: [{ node: { name: "a298", born: 10298 } }] } } } + { name: "s299", born: 299, likes: { Person: { create: [{ node: { name: "a299", born: 10299 } }] } } } + ] + ) { + people { + name + } + } +} diff --git a/packages/graphql/tests/performance/graphql/batch-create.graphql b/packages/graphql/tests/performance/graphql/batch-create/batch-create.graphql similarity index 83% rename from packages/graphql/tests/performance/graphql/batch-create.graphql rename to packages/graphql/tests/performance/graphql/batch-create/batch-create.graphql index ce7d55d411..2b767d2746 100644 --- a/packages/graphql/tests/performance/graphql/batch-create.graphql +++ b/packages/graphql/tests/performance/graphql/batch-create/batch-create.graphql @@ -1,4 +1,4 @@ -mutation BatchCreate_skip { +mutation BatchCreate { createMovies( input: [ { id: 0, title: "The Matrix 0" } @@ -502,6 +502,105 @@ mutation BatchCreate_skip { { id: 498, title: "The Matrix 498" } { id: 499, title: "The Matrix 499" } { id: 500, title: "The Matrix 500" } + { id: 501, title: "The Matrix 501" } + { id: 502, title: "The Matrix 502" } + { id: 503, title: "The Matrix 503" } + { id: 504, title: "The Matrix 504" } + { id: 505, title: "The Matrix 505" } + { id: 506, title: "The Matrix 506" } + { id: 507, title: "The Matrix 507" } + { id: 508, title: "The Matrix 508" } + { id: 509, title: "The Matrix 509" } + { id: 510, title: "The Matrix 510" } + { id: 511, title: "The Matrix 511" } + { id: 512, title: "The Matrix 512" } + { id: 513, title: "The Matrix 513" } + { id: 514, title: "The Matrix 514" } + { id: 515, title: "The Matrix 515" } + { id: 516, title: "The Matrix 516" } + { id: 517, title: "The Matrix 517" } + { id: 518, title: "The Matrix 518" } + { id: 519, title: "The Matrix 519" } + { id: 520, title: "The Matrix 520" } + { id: 521, title: "The Matrix 521" } + { id: 522, title: "The Matrix 522" } + { id: 523, title: "The Matrix 523" } + { id: 524, title: "The Matrix 524" } + { id: 525, title: "The Matrix 525" } + { id: 526, title: "The Matrix 526" } + { id: 527, title: "The Matrix 527" } + { id: 528, title: "The Matrix 528" } + { id: 529, title: "The Matrix 529" } + { id: 530, title: "The Matrix 530" } + { id: 531, title: "The Matrix 531" } + { id: 532, title: "The Matrix 532" } + { id: 533, title: "The Matrix 533" } + { id: 534, title: "The Matrix 534" } + { id: 535, title: "The Matrix 535" } + { id: 536, title: "The Matrix 536" } + { id: 537, title: "The Matrix 537" } + { id: 538, title: "The Matrix 538" } + { id: 539, title: "The Matrix 539" } + { id: 540, title: "The Matrix 540" } + { id: 541, title: "The Matrix 541" } + { id: 542, title: "The Matrix 542" } + { id: 543, title: "The Matrix 543" } + { id: 544, title: "The Matrix 544" } + { id: 545, title: "The Matrix 545" } + { id: 546, title: "The Matrix 546" } + { id: 547, title: "The Matrix 547" } + { id: 548, title: "The Matrix 548" } + { id: 549, title: "The Matrix 549" } + { id: 550, title: "The Matrix 550" } + { id: 551, title: "The Matrix 551" } + { id: 552, title: "The Matrix 552" } + { id: 553, title: "The Matrix 553" } + { id: 554, title: "The Matrix 554" } + { id: 555, title: "The Matrix 555" } + { id: 556, title: "The Matrix 556" } + { id: 557, title: "The Matrix 557" } + { id: 558, title: "The Matrix 558" } + { id: 559, title: "The Matrix 559" } + { id: 560, title: "The Matrix 560" } + { id: 561, title: "The Matrix 561" } + { id: 562, title: "The Matrix 562" } + { id: 563, title: "The Matrix 563" } + { id: 564, title: "The Matrix 564" } + { id: 565, title: "The Matrix 565" } + { id: 566, title: "The Matrix 566" } + { id: 567, title: "The Matrix 567" } + { id: 568, title: "The Matrix 568" } + { id: 569, title: "The Matrix 569" } + { id: 570, title: "The Matrix 570" } + { id: 571, title: "The Matrix 571" } + { id: 572, title: "The Matrix 572" } + { id: 573, title: "The Matrix 573" } + { id: 574, title: "The Matrix 574" } + { id: 575, title: "The Matrix 575" } + { id: 576, title: "The Matrix 576" } + { id: 577, title: "The Matrix 577" } + { id: 578, title: "The Matrix 578" } + { id: 579, title: "The Matrix 579" } + { id: 580, title: "The Matrix 580" } + { id: 581, title: "The Matrix 581" } + { id: 582, title: "The Matrix 582" } + { id: 583, title: "The Matrix 583" } + { id: 584, title: "The Matrix 584" } + { id: 585, title: "The Matrix 585" } + { id: 586, title: "The Matrix 586" } + { id: 587, title: "The Matrix 587" } + { id: 588, title: "The Matrix 588" } + { id: 589, title: "The Matrix 589" } + { id: 590, title: "The Matrix 590" } + { id: 591, title: "The Matrix 591" } + { id: 592, title: "The Matrix 592" } + { id: 593, title: "The Matrix 593" } + { id: 594, title: "The Matrix 594" } + { id: 595, title: "The Matrix 595" } + { id: 596, title: "The Matrix 596" } + { id: 597, title: "The Matrix 597" } + { id: 598, title: "The Matrix 598" } + { id: 599, title: "The Matrix 599" } ] ) { movies { diff --git a/packages/graphql/tests/tck/connections/projections/create.test.ts b/packages/graphql/tests/tck/connections/projections/create.test.ts index f5d3af0c10..efdad53c07 100644 --- a/packages/graphql/tests/tck/connections/projections/create.test.ts +++ b/packages/graphql/tests/tck/connections/projections/create.test.ts @@ -82,26 +82,32 @@ describe("Cypher -> Connections -> Projections -> Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.title = $this0_title - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.title = create_var1.title + RETURN create_this0 } CALL { - WITH this0 - MATCH (this0)<-[this0_connection_actorsConnectionthis0:ACTED_IN]-(this0_Actor:\`Actor\`) - WITH { screenTime: this0_connection_actorsConnectionthis0.screenTime, node: { name: this0_Actor.name } } AS edge + WITH create_this0 + MATCH (create_this0)<-[create_this0_connection_actorsConnectionthis0:ACTED_IN]-(create_this0_Actor:\`Actor\`) + WITH { screenTime: create_this0_connection_actorsConnectionthis0.screenTime, node: { name: create_this0_Actor.name } } AS edge WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_actorsConnection + RETURN { edges: edges, totalCount: totalCount } AS create_this0_actorsConnection } - RETURN [ - this0 { .title, actorsConnection: this0_actorsConnection }] AS data" + RETURN collect(create_this0 { .title, actorsConnection: create_this0_actorsConnection }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_title\\": \\"Forrest Gump\\", + \\"create_param0\\": [ + { + \\"title\\": \\"Forrest Gump\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -132,41 +138,35 @@ describe("Cypher -> Connections -> Projections -> Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.title = $this0_title - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:Movie) - SET this1.title = $this1_title - RETURN this1 - } - CALL { - WITH this0 - MATCH (this0)<-[this0_connection_actorsConnectionthis0:ACTED_IN]-(this0_Actor:\`Actor\`) - WITH { screenTime: this0_connection_actorsConnectionthis0.screenTime, node: { name: this0_Actor.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_actorsConnection + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.title = create_var1.title + RETURN create_this0 } CALL { - WITH this1 - MATCH (this1)<-[this1_connection_actorsConnectionthis0:ACTED_IN]-(this1_Actor:\`Actor\`) - WITH { screenTime: this1_connection_actorsConnectionthis0.screenTime, node: { name: this1_Actor.name } } AS edge + WITH create_this0 + MATCH (create_this0)<-[create_this0_connection_actorsConnectionthis0:ACTED_IN]-(create_this0_Actor:\`Actor\`) + WITH { screenTime: create_this0_connection_actorsConnectionthis0.screenTime, node: { name: create_this0_Actor.name } } AS edge WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this1_actorsConnection + RETURN { edges: edges, totalCount: totalCount } AS create_this0_actorsConnection } - RETURN [ - this0 { .title, actorsConnection: this0_actorsConnection }, - this1 { .title, actorsConnection: this1_actorsConnection }] AS data" + RETURN collect(create_this0 { .title, actorsConnection: create_this0_actorsConnection }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_title\\": \\"Forrest Gump\\", - \\"this1_title\\": \\"Toy Story\\", + \\"create_param0\\": [ + { + \\"title\\": \\"Forrest Gump\\" + }, + { + \\"title\\": \\"Toy Story\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -197,43 +197,36 @@ describe("Cypher -> Connections -> Projections -> Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.title = $this0_title - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:Movie) - SET this1.title = $this1_title - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.title = create_var1.title + RETURN create_this0 } CALL { - WITH this0 - MATCH (this0)<-[this0_connection_actorsConnectionthis0:ACTED_IN]-(this0_Actor:\`Actor\`) - WHERE this0_Actor.name = $projection_connection_actorsConnectionparam0 - WITH { screenTime: this0_connection_actorsConnectionthis0.screenTime, node: { name: this0_Actor.name } } AS edge + WITH create_this0 + MATCH (create_this0)<-[create_this0_connection_actorsConnectionthis0:ACTED_IN]-(create_this0_Actor:\`Actor\`) + WHERE create_this0_Actor.name = $projection_connection_actorsConnectionparam0 + WITH { screenTime: create_this0_connection_actorsConnectionthis0.screenTime, node: { name: create_this0_Actor.name } } AS edge WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_actorsConnection + RETURN { edges: edges, totalCount: totalCount } AS create_this0_actorsConnection } - CALL { - WITH this1 - MATCH (this1)<-[this1_connection_actorsConnectionthis0:ACTED_IN]-(this1_Actor:\`Actor\`) - WHERE this1_Actor.name = $projection_connection_actorsConnectionparam0 - WITH { screenTime: this1_connection_actorsConnectionthis0.screenTime, node: { name: this1_Actor.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this1_actorsConnection - } - RETURN [ - this0 { .title, actorsConnection: this0_actorsConnection }, - this1 { .title, actorsConnection: this1_actorsConnection }] AS data" + RETURN collect(create_this0 { .title, actorsConnection: create_this0_actorsConnection }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_title\\": \\"Forrest Gump\\", - \\"this1_title\\": \\"Toy Story\\", + \\"create_param0\\": [ + { + \\"title\\": \\"Forrest Gump\\" + }, + { + \\"title\\": \\"Toy Story\\" + } + ], \\"projection_connection_actorsConnectionparam0\\": \\"Tom Hanks\\", \\"resolvedCallbacks\\": {} }" diff --git a/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts b/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts index ddde79966c..df9a1a6fcc 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts @@ -89,36 +89,60 @@ describe("Relationship Properties Create Cypher", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.title = $this0_title - WITH this0 - CREATE (this0_actors0_node:Actor) - SET this0_actors0_node.name = $this0_actors0_node_name - MERGE (this0)<-[this0_actors0_relationship:ACTED_IN]-(this0_actors0_node) - SET this0_actors0_relationship.screenTime = $this0_actors0_relationship_screenTime - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.title = create_var1.title + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`) + SET + create_this5.name = create_var3.name + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + SET + create_this6.screenTime = create_var4.screenTime + RETURN collect(NULL) + } + RETURN create_this0 } CALL { - WITH this0 - MATCH (this0)<-[this0_connection_actorsConnectionthis0:ACTED_IN]-(this0_Actor:\`Actor\`) - WITH { screenTime: this0_connection_actorsConnectionthis0.screenTime, node: { name: this0_Actor.name } } AS edge + WITH create_this0 + MATCH (create_this0)<-[create_this0_connection_actorsConnectionthis0:ACTED_IN]-(create_this0_Actor:\`Actor\`) + WITH { screenTime: create_this0_connection_actorsConnectionthis0.screenTime, node: { name: create_this0_Actor.name } } AS edge WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_actorsConnection + RETURN { edges: edges, totalCount: totalCount } AS create_this0_actorsConnection } - RETURN [ - this0 { .title, actorsConnection: this0_actorsConnection }] AS data" + RETURN collect(create_this0 { .title, actorsConnection: create_this0_actorsConnection }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_title\\": \\"Forrest Gump\\", - \\"this0_actors0_node_name\\": \\"Tom Hanks\\", - \\"this0_actors0_relationship_screenTime\\": { - \\"low\\": 60, - \\"high\\": 0 - }, + \\"create_param0\\": [ + { + \\"title\\": \\"Forrest Gump\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"Tom Hanks\\" + }, + \\"edge\\": { + \\"screenTime\\": { + \\"low\\": 60, + \\"high\\": 0 + } + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/deprecated/node-plural.test.ts b/packages/graphql/tests/tck/deprecated/node-plural.test.ts index 697d1817e2..7ef37c46cd 100644 --- a/packages/graphql/tests/tck/deprecated/node-plural.test.ts +++ b/packages/graphql/tests/tck/deprecated/node-plural.test.ts @@ -108,18 +108,24 @@ describe("Plural in Node directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:\`Tech\`) - SET this0.name = $this0_name - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Tech\`) + SET + create_this0.name = create_var1.name + RETURN create_this0 } - RETURN [ - this0 { .name }] AS data" + RETURN collect(create_this0 { .name }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_name\\": \\"Highlander\\", + \\"create_param0\\": [ + { + \\"name\\": \\"Highlander\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/alias.test.ts b/packages/graphql/tests/tck/directives/alias.test.ts index e547197702..7a3fd869b3 100644 --- a/packages/graphql/tests/tck/directives/alias.test.ts +++ b/packages/graphql/tests/tck/directives/alias.test.ts @@ -171,48 +171,72 @@ describe("Cypher alias directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Actor) - SET this0.name = $this0_name - SET this0.cityPropInDb = $this0_city - WITH this0 - CREATE (this0_actedIn0_node:Movie) - SET this0_actedIn0_node.title = $this0_actedIn0_node_title - SET this0_actedIn0_node.ratingPropInDb = $this0_actedIn0_node_rating - MERGE (this0)-[this0_actedIn0_relationship:ACTED_IN]->(this0_actedIn0_node) - SET this0_actedIn0_relationship.characterPropInDb = $this0_actedIn0_relationship_character - SET this0_actedIn0_relationship.screenTime = $this0_actedIn0_relationship_screenTime - RETURN this0 + "UNWIND $create_param0 AS create_var2 + CALL { + WITH create_var2 + CREATE (create_this1:\`Actor\`) + SET + create_this1.name = create_var2.name, + create_this1.cityPropInDb = create_var2.city + WITH create_this1, create_var2 + CALL { + WITH create_this1, create_var2 + UNWIND create_var2.actedIn.create AS create_var3 + WITH create_var3.node AS create_var4, create_var3.edge AS create_var5, create_this1 + CREATE (create_this6:\`Movie\`) + SET + create_this6.title = create_var4.title, + create_this6.ratingPropInDb = create_var4.rating + MERGE (create_this1)-[create_this7:ACTED_IN]->(create_this6) + SET + create_this7.characterPropInDb = create_var5.character, + create_this7.screenTime = create_var5.screenTime + RETURN collect(NULL) + } + RETURN create_this1 } CALL { - WITH this0 - MATCH (this0)-[create_this0:ACTED_IN]->(this0_actedIn:\`Movie\`) - WITH this0_actedIn { .title, rating: this0_actedIn.ratingPropInDb } AS this0_actedIn - RETURN collect(this0_actedIn) AS this0_actedIn + WITH create_this1 + MATCH (create_this1)-[create_this0:ACTED_IN]->(create_this1_actedIn:\`Movie\`) + WITH create_this1_actedIn { .title, rating: create_this1_actedIn.ratingPropInDb } AS create_this1_actedIn + RETURN collect(create_this1_actedIn) AS create_this1_actedIn } CALL { - WITH this0 - MATCH (this0)-[this0_connection_actedInConnectionthis0:ACTED_IN]->(this0_Movie:\`Movie\`) - WITH { character: this0_connection_actedInConnectionthis0.characterPropInDb, screenTime: this0_connection_actedInConnectionthis0.screenTime, node: { title: this0_Movie.title, rating: this0_Movie.ratingPropInDb } } AS edge + WITH create_this1 + MATCH (create_this1)-[create_this1_connection_actedInConnectionthis0:ACTED_IN]->(create_this1_Movie:\`Movie\`) + WITH { character: create_this1_connection_actedInConnectionthis0.characterPropInDb, screenTime: create_this1_connection_actedInConnectionthis0.screenTime, node: { title: create_this1_Movie.title, rating: create_this1_Movie.ratingPropInDb } } AS edge WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_actedInConnection + RETURN { edges: edges, totalCount: totalCount } AS create_this1_actedInConnection } - RETURN [ - this0 { .name, city: this0.cityPropInDb, actedIn: this0_actedIn, actedInConnection: this0_actedInConnection }] AS data" + RETURN collect(create_this1 { .name, city: create_this1.cityPropInDb, actedIn: create_this1_actedIn, actedInConnection: create_this1_actedInConnection }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_name\\": \\"Molly\\", - \\"this0_city\\": \\"Sjömarken\\", - \\"this0_actedIn0_node_title\\": \\"Molly's game\\", - \\"this0_actedIn0_node_rating\\": 5, - \\"this0_actedIn0_relationship_character\\": \\"Molly\\", - \\"this0_actedIn0_relationship_screenTime\\": { - \\"low\\": 120, - \\"high\\": 0 - }, + \\"create_param0\\": [ + { + \\"name\\": \\"Molly\\", + \\"city\\": \\"Sjömarken\\", + \\"actedIn\\": { + \\"create\\": [ + { + \\"node\\": { + \\"title\\": \\"Molly's game\\", + \\"rating\\": 5 + }, + \\"edge\\": { + \\"character\\": \\"Molly\\", + \\"screenTime\\": { + \\"low\\": 120, + \\"high\\": 0 + } + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/auth/arguments/allow/allow.test.ts b/packages/graphql/tests/tck/directives/auth/arguments/allow/allow.test.ts index 86283a1791..804ca1c347 100644 --- a/packages/graphql/tests/tck/directives/auth/arguments/allow/allow.test.ts +++ b/packages/graphql/tests/tck/directives/auth/arguments/allow/allow.test.ts @@ -545,24 +545,24 @@ describe("Cypher Auth Allow", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) -WHERE this_posts0.id = $this_deleteUsers_args_delete_posts0_where_Postparam0 -WITH this, this_posts0 -CALL apoc.util.validate(NOT ((exists((this_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete -CALL { - WITH this_posts0_to_delete - UNWIND this_posts0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this -CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -DETACH DELETE this" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) + WHERE this_posts0.id = $this_deleteUsers_args_delete_posts0_where_Postparam0 + WITH this, this_posts0 + CALL apoc.util.validate(NOT ((exists((this_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete + CALL { + WITH this_posts0_to_delete + UNWIND this_posts0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this + CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -606,27 +606,27 @@ DETACH DELETE this" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) -WHERE this_disconnect_posts0.id = $updateUsers_args_disconnect_posts0_where_Postparam0 -WITH this, this_disconnect_posts0, this_disconnect_posts0_rel -CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0) AND (exists((this_disconnect_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_disconnect_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_disconnect_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_disconnect_posts0, this_disconnect_posts0_rel - WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel - UNWIND this_disconnect_posts0 as x - DELETE this_disconnect_posts0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) + WHERE this_disconnect_posts0.id = $updateUsers_args_disconnect_posts0_where_Postparam0 + WITH this, this_disconnect_posts0, this_disconnect_posts0_rel + CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0) AND (exists((this_disconnect_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_disconnect_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_disconnect_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_disconnect_posts0, this_disconnect_posts0_rel + WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel + UNWIND this_disconnect_posts0 as x + DELETE this_disconnect_posts0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -676,58 +676,58 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Comment\`) -WHERE this.id = $param0 -WITH this -CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) -WITH this, this_post0_disconnect0, this_post0_disconnect0_rel -CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0))) AND (exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_post0_disconnect0, this_post0_disconnect0_rel - WITH collect(this_post0_disconnect0) as this_post0_disconnect0, this_post0_disconnect0_rel - UNWIND this_post0_disconnect0 as x - DELETE this_post0_disconnect0_rel - RETURN count(*) AS _ -} -WITH this, this_post0_disconnect0 -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_post_disconnect_disconnect_creator_where_Userparam0 -WITH this, this_post0_disconnect0, this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel -CALL apoc.util.validate(NOT ((exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0))) AND (this_post0_disconnect0_creator0.id IS NOT NULL AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0auth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel - WITH collect(this_post0_disconnect0_creator0) as this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel - UNWIND this_post0_disconnect0_creator0 as x - DELETE this_post0_disconnect0_creator0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_post0_disconnect0_creator_User -} -RETURN count(*) AS disconnect_this_post0_disconnect_Post -} -WITH this -CALL { - WITH this - MATCH (this)<-[this_creator_User_unique:HAS_COMMENT]-(:User) - WITH count(this_creator_User_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.creator required', [0]) - RETURN c AS this_creator_User_unique_ignored -} -CALL { - WITH this - MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) - WITH count(this_post_Post_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) - RETURN c AS this_post_Post_unique_ignored -} -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`Comment\`) + WHERE this.id = $param0 + WITH this + CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) + WITH this, this_post0_disconnect0, this_post0_disconnect0_rel + CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0))) AND (exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_post0_disconnect0, this_post0_disconnect0_rel + WITH collect(this_post0_disconnect0) as this_post0_disconnect0, this_post0_disconnect0_rel + UNWIND this_post0_disconnect0 as x + DELETE this_post0_disconnect0_rel + RETURN count(*) AS _ + } + WITH this, this_post0_disconnect0 + 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_post_disconnect_disconnect_creator_where_Userparam0 + WITH this, this_post0_disconnect0, this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + CALL apoc.util.validate(NOT ((exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0))) AND (this_post0_disconnect0_creator0.id IS NOT NULL AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0auth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + WITH collect(this_post0_disconnect0_creator0) as this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + UNWIND this_post0_disconnect0_creator0 as x + DELETE this_post0_disconnect0_creator0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_post0_disconnect0_creator_User + } + RETURN count(*) AS disconnect_this_post0_disconnect_Post + } + WITH this + CALL { + WITH this + MATCH (this)<-[this_creator_User_unique:HAS_COMMENT]-(:User) + WITH count(this_creator_User_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.creator required', [0]) + RETURN c AS this_creator_User_unique_ignored + } + CALL { + WITH this + MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) + WITH count(this_post_Post_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) + RETURN c AS this_post_Post_unique_ignored + } + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -777,28 +777,28 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_posts0_node:Post) - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 - WITH this, this_connect_posts0_node - CALL apoc.util.validate(NOT ((exists((this_connect_posts0_node)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_connect_posts0_nodeauth_param0))) AND (this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH * - WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_posts0_node - MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_posts0_node:Post) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 + WITH this, this_connect_posts0_node + CALL apoc.util.validate(NOT ((exists((this_connect_posts0_node)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_connect_posts0_nodeauth_param0))) AND (this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_posts0_node + MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/auth/arguments/allow/inherited.test.ts b/packages/graphql/tests/tck/directives/auth/arguments/allow/inherited.test.ts index b526e05450..9a487f5d3f 100644 --- a/packages/graphql/tests/tck/directives/auth/arguments/allow/inherited.test.ts +++ b/packages/graphql/tests/tck/directives/auth/arguments/allow/inherited.test.ts @@ -538,24 +538,24 @@ describe("@auth allow when inherited from interface", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) -WHERE this_posts0.id = $this_deleteUsers_args_delete_posts0_where_Postparam0 -WITH this, this_posts0 -CALL apoc.util.validate(NOT ((exists((this_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete -CALL { - WITH this_posts0_to_delete - UNWIND this_posts0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this -CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -DETACH DELETE this" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) + WHERE this_posts0.id = $this_deleteUsers_args_delete_posts0_where_Postparam0 + WITH this, this_posts0 + CALL apoc.util.validate(NOT ((exists((this_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete + CALL { + WITH this_posts0_to_delete + UNWIND this_posts0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this + CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -599,27 +599,27 @@ DETACH DELETE this" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) -WHERE this_disconnect_posts0.id = $updateUsers_args_disconnect_posts0_where_Postparam0 -WITH this, this_disconnect_posts0, this_disconnect_posts0_rel -CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0) AND (exists((this_disconnect_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_disconnect_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_disconnect_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_disconnect_posts0, this_disconnect_posts0_rel - WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel - UNWIND this_disconnect_posts0 as x - DELETE this_disconnect_posts0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) + WHERE this_disconnect_posts0.id = $updateUsers_args_disconnect_posts0_where_Postparam0 + WITH this, this_disconnect_posts0, this_disconnect_posts0_rel + CALL apoc.util.validate(NOT ((this.id IS NOT NULL AND this.id = $thisauth_param0) AND (exists((this_disconnect_posts0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_disconnect_posts0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_disconnect_posts0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_disconnect_posts0, this_disconnect_posts0_rel + WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel + UNWIND this_disconnect_posts0 as x + DELETE this_disconnect_posts0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -669,58 +669,58 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Comment\`) -WHERE this.id = $param0 -WITH this -CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) -WITH this, this_post0_disconnect0, this_post0_disconnect0_rel -CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0))) AND (exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_post0_disconnect0, this_post0_disconnect0_rel - WITH collect(this_post0_disconnect0) as this_post0_disconnect0, this_post0_disconnect0_rel - UNWIND this_post0_disconnect0 as x - DELETE this_post0_disconnect0_rel - RETURN count(*) AS _ -} -WITH this, this_post0_disconnect0 -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_post_disconnect_disconnect_creator_where_Userparam0 -WITH this, this_post0_disconnect0, this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel -CALL apoc.util.validate(NOT ((exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0))) AND (this_post0_disconnect0_creator0.id IS NOT NULL AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0auth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel - WITH collect(this_post0_disconnect0_creator0) as this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel - UNWIND this_post0_disconnect0_creator0 as x - DELETE this_post0_disconnect0_creator0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_post0_disconnect0_creator_User -} -RETURN count(*) AS disconnect_this_post0_disconnect_Post -} -WITH this -CALL { - WITH this - MATCH (this)<-[this_creator_User_unique:HAS_COMMENT]-(:User) - WITH count(this_creator_User_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.creator required', [0]) - RETURN c AS this_creator_User_unique_ignored -} -CALL { - WITH this - MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) - WITH count(this_post_Post_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) - RETURN c AS this_post_Post_unique_ignored -} -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`Comment\`) + WHERE this.id = $param0 + WITH this + CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_post0_disconnect0_rel:HAS_COMMENT]-(this_post0_disconnect0:Post) + WITH this, this_post0_disconnect0, this_post0_disconnect0_rel + CALL apoc.util.validate(NOT ((exists((this)<-[:HAS_COMMENT]-(:\`User\`)) AND any(auth_this0 IN [(this)<-[:HAS_COMMENT]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $thisauth_param0))) AND (exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0)))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_post0_disconnect0, this_post0_disconnect0_rel + WITH collect(this_post0_disconnect0) as this_post0_disconnect0, this_post0_disconnect0_rel + UNWIND this_post0_disconnect0 as x + DELETE this_post0_disconnect0_rel + RETURN count(*) AS _ + } + WITH this, this_post0_disconnect0 + 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_post_disconnect_disconnect_creator_where_Userparam0 + WITH this, this_post0_disconnect0, this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + CALL apoc.util.validate(NOT ((exists((this_post0_disconnect0)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_post0_disconnect0)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_post0_disconnect0auth_param0))) AND (this_post0_disconnect0_creator0.id IS NOT NULL AND this_post0_disconnect0_creator0.id = $this_post0_disconnect0_creator0auth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + WITH collect(this_post0_disconnect0_creator0) as this_post0_disconnect0_creator0, this_post0_disconnect0_creator0_rel + UNWIND this_post0_disconnect0_creator0 as x + DELETE this_post0_disconnect0_creator0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_post0_disconnect0_creator_User + } + RETURN count(*) AS disconnect_this_post0_disconnect_Post + } + WITH this + CALL { + WITH this + MATCH (this)<-[this_creator_User_unique:HAS_COMMENT]-(:User) + WITH count(this_creator_User_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.creator required', [0]) + RETURN c AS this_creator_User_unique_ignored + } + CALL { + WITH this + MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) + WITH count(this_post_Post_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) + RETURN c AS this_post_Post_unique_ignored + } + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -770,28 +770,28 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WHERE this.id = $param0 -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_posts0_node:Post) - WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 - WITH this, this_connect_posts0_node - CALL apoc.util.validate(NOT ((exists((this_connect_posts0_node)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_connect_posts0_nodeauth_param0))) AND (this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH * - WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_posts0_node - MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_posts0_node:Post) + WHERE this_connect_posts0_node.id = $this_connect_posts0_node_param0 + WITH this, this_connect_posts0_node + CALL apoc.util.validate(NOT ((exists((this_connect_posts0_node)<-[:HAS_POST]-(:\`User\`)) AND any(auth_this0 IN [(this_connect_posts0_node)<-[:HAS_POST]-(auth_this0:\`User\`) | auth_this0] WHERE (auth_this0.id IS NOT NULL AND auth_this0.id = $this_connect_posts0_nodeauth_param0))) AND (this.id IS NOT NULL AND this.id = $thisauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_posts0_node + MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts b/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts index 385c87c9dc..6d30485e17 100644 --- a/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts +++ b/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/interface-relationships/implementation-is-authenticated.test.ts @@ -139,20 +139,26 @@ describe("Cypher Auth isAuthenticated", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Comment) - SET this0.id = $this0_id - SET this0.content = $this0_content - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Comment\`) + SET + create_this0.id = create_var1.id, + create_this0.content = create_var1.content + RETURN create_this0 } - RETURN [ - this0 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this0_content\\": \\"content\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"content\\": \\"content\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -253,39 +259,39 @@ describe("Cypher Auth isAuthenticated", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_content0_node:Comment) - CALL { - WITH * - WITH collect(this_connect_content0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_content0_node - MERGE (this)-[:HAS_CONTENT]->(this_connect_content0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_content_Comment -} -CALL { - WITH this - OPTIONAL MATCH (this_connect_content0_node:Post) - WITH this, this_connect_content0_node - CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH * - WITH collect(this_connect_content0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_content0_node - MERGE (this)-[:HAS_CONTENT]->(this_connect_content0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_content_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_content0_node:Comment) + CALL { + WITH * + WITH collect(this_connect_content0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_content0_node + MERGE (this)-[:HAS_CONTENT]->(this_connect_content0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_content_Comment + } + CALL { + WITH this + OPTIONAL MATCH (this_connect_content0_node:Post) + WITH this, this_connect_content0_node + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this_connect_content0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_content0_node + MERGE (this)-[:HAS_CONTENT]->(this_connect_content0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_content_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -323,37 +329,37 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)-[this_disconnect_content0_rel:HAS_CONTENT]->(this_disconnect_content0:Comment) -CALL { - WITH this_disconnect_content0, this_disconnect_content0_rel - WITH collect(this_disconnect_content0) as this_disconnect_content0, this_disconnect_content0_rel - UNWIND this_disconnect_content0 as x - DELETE this_disconnect_content0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_content_Comment -} -CALL { - WITH this -OPTIONAL MATCH (this)-[this_disconnect_content0_rel:HAS_CONTENT]->(this_disconnect_content0:Post) -WITH this, this_disconnect_content0, this_disconnect_content0_rel -CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_disconnect_content0, this_disconnect_content0_rel - WITH collect(this_disconnect_content0) as this_disconnect_content0, this_disconnect_content0_rel - UNWIND this_disconnect_content0 as x - DELETE this_disconnect_content0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_content_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_content0_rel:HAS_CONTENT]->(this_disconnect_content0:Comment) + CALL { + WITH this_disconnect_content0, this_disconnect_content0_rel + WITH collect(this_disconnect_content0) as this_disconnect_content0, this_disconnect_content0_rel + UNWIND this_disconnect_content0 as x + DELETE this_disconnect_content0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_content_Comment + } + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_content0_rel:HAS_CONTENT]->(this_disconnect_content0:Post) + WITH this, this_disconnect_content0, this_disconnect_content0_rel + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_disconnect_content0, this_disconnect_content0_rel + WITH collect(this_disconnect_content0) as this_disconnect_content0, this_disconnect_content0_rel + UNWIND this_disconnect_content0 as x + DELETE this_disconnect_content0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_content_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -459,29 +465,29 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -OPTIONAL MATCH (this)-[this_content_Comment0_relationship:HAS_CONTENT]->(this_content_Comment0:Comment) -WITH this, collect(DISTINCT this_content_Comment0) as this_content_Comment0_to_delete -CALL { - WITH this_content_Comment0_to_delete - UNWIND this_content_Comment0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this -OPTIONAL MATCH (this)-[this_content_Post0_relationship:HAS_CONTENT]->(this_content_Post0:Post) -WITH this, this_content_Post0 -CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this, collect(DISTINCT this_content_Post0) as this_content_Post0_to_delete -CALL { - WITH this_content_Post0_to_delete - UNWIND this_content_Post0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -DETACH DELETE this" -`); + "MATCH (this:\`User\`) + WITH this + OPTIONAL MATCH (this)-[this_content_Comment0_relationship:HAS_CONTENT]->(this_content_Comment0:Comment) + WITH this, collect(DISTINCT this_content_Comment0) as this_content_Comment0_to_delete + CALL { + WITH this_content_Comment0_to_delete + UNWIND this_content_Comment0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this + OPTIONAL MATCH (this)-[this_content_Post0_relationship:HAS_CONTENT]->(this_content_Post0:Post) + WITH this, this_content_Post0 + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this, collect(DISTINCT this_content_Post0) as this_content_Post0_to_delete + CALL { + WITH this_content_Post0_to_delete + UNWIND this_content_Post0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/is-authenticated.test.ts b/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/is-authenticated.test.ts index 331325f7a4..c6bc1a3016 100644 --- a/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/is-authenticated.test.ts +++ b/packages/graphql/tests/tck/directives/auth/arguments/is-authenticated/is-authenticated.test.ts @@ -411,26 +411,26 @@ describe("Cypher Auth isAuthenticated", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_posts0_node:Post) - WITH this, this_connect_posts0_node - CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0]) AND apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH * - WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_posts0_node - MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_posts0_node:Post) + WITH this, this_connect_posts0_node + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0]) AND apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_posts0_node + MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -468,25 +468,25 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) -WITH this, this_disconnect_posts0, this_disconnect_posts0_rel -CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0]) AND apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_disconnect_posts0, this_disconnect_posts0_rel - WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel - UNWIND this_disconnect_posts0 as x - DELETE this_disconnect_posts0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) + WITH this, this_disconnect_posts0, this_disconnect_posts0_rel + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0]) AND apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_disconnect_posts0, this_disconnect_posts0_rel + WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel + UNWIND this_disconnect_posts0 as x + DELETE this_disconnect_posts0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -570,22 +570,22 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) -WITH this, this_posts0 -CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete -CALL { - WITH this_posts0_to_delete - UNWIND this_posts0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this -CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -DETACH DELETE this" -`); + "MATCH (this:\`User\`) + WITH this + OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) + WITH this, this_posts0 + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete + CALL { + WITH this_posts0_to_delete + UNWIND this_posts0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this + CALL apoc.util.validate(NOT (apoc.util.validatePredicate(NOT ($auth.isAuthenticated = true), \\"@neo4j/graphql/UNAUTHENTICATED\\", [0])), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/auth/arguments/roles/roles.test.ts b/packages/graphql/tests/tck/directives/auth/arguments/roles/roles.test.ts index afa2f9ce8d..fe29996286 100644 --- a/packages/graphql/tests/tck/directives/auth/arguments/roles/roles.test.ts +++ b/packages/graphql/tests/tck/directives/auth/arguments/roles/roles.test.ts @@ -417,26 +417,26 @@ describe("Cypher Auth Roles", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_posts0_node:Post) - WITH this, this_connect_posts0_node - CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - CALL { - WITH * - WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_posts0_node - MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_posts0_node:Post) + WITH this, this_connect_posts0_node + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this_connect_posts0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_posts0_node + MERGE (this)-[:HAS_POST]->(this_connect_posts0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -478,48 +478,48 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Comment\`) -WITH this -OPTIONAL MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) -CALL apoc.do.when(this_post0 IS NOT NULL, \\" -WITH this, this_post0 -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 - WITH this, this_post0, this_post0_creator0_connect0_node - CALL apoc.util.validate(NOT (any(auth_var1 IN [\\\\\\"admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\\\\\"super-admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\\\\\"@neo4j/graphql/FORBIDDEN\\\\\\", [0]) - CALL { - WITH * - WITH this, collect(this_post0_creator0_connect0_node) as connectedNodes, collect(this_post0) as parentNodes - UNWIND parentNodes as this_post0 - UNWIND connectedNodes as this_post0_creator0_connect0_node - MERGE (this_post0)-[:HAS_POST]->(this_post0_creator0_connect0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_post0_creator0_connect_User -} -WITH this, this_post0 -CALL { - WITH this_post0 - MATCH (this_post0)-[this_post0_creator_User_unique:HAS_POST]->(:User) - WITH count(this_post0_creator_User_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDPost.creator required', [0]) - RETURN c AS this_post0_creator_User_unique_ignored -} -RETURN count(*) AS _ -\\", \\"\\", {this:this, updateComments: $updateComments, this_post0:this_post0, auth:$auth,this_post0_creator0_connect0_node_param0:$this_post0_creator0_connect0_node_param0}) -YIELD value AS _ -WITH this -CALL { - WITH this - MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) - WITH count(this_post_Post_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) - RETURN c AS this_post_Post_unique_ignored -} -RETURN collect(DISTINCT this { .content }) AS data" -`); + "MATCH (this:\`Comment\`) + WITH this + OPTIONAL MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) + CALL apoc.do.when(this_post0 IS NOT NULL, \\" + WITH this, this_post0 + 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 + WITH this, this_post0, this_post0_creator0_connect0_node + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\\\\\"admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\\\\\"super-admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\\\\\"@neo4j/graphql/FORBIDDEN\\\\\\", [0]) + CALL { + WITH * + WITH this, collect(this_post0_creator0_connect0_node) as connectedNodes, collect(this_post0) as parentNodes + UNWIND parentNodes as this_post0 + UNWIND connectedNodes as this_post0_creator0_connect0_node + MERGE (this_post0)-[:HAS_POST]->(this_post0_creator0_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_post0_creator0_connect_User + } + WITH this, this_post0 + CALL { + WITH this_post0 + MATCH (this_post0)-[this_post0_creator_User_unique:HAS_POST]->(:User) + WITH count(this_post0_creator_User_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDPost.creator required', [0]) + RETURN c AS this_post0_creator_User_unique_ignored + } + RETURN count(*) AS _ + \\", \\"\\", {this:this, updateComments: $updateComments, this_post0:this_post0, auth:$auth,this_post0_creator0_connect0_node_param0:$this_post0_creator0_connect0_node_param0}) + YIELD value AS _ + WITH this + CALL { + WITH this + MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) + WITH count(this_post_Post_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) + RETURN c AS this_post_Post_unique_ignored + } + RETURN collect(DISTINCT this { .content }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -579,25 +579,25 @@ RETURN collect(DISTINCT this { .content }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) -WITH this, this_disconnect_posts0, this_disconnect_posts0_rel -CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -CALL { - WITH this_disconnect_posts0, this_disconnect_posts0_rel - WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel - UNWIND this_disconnect_posts0 as x - DELETE this_disconnect_posts0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_posts_Post -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`User\`) + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)-[this_disconnect_posts0_rel:HAS_POST]->(this_disconnect_posts0:Post) + WITH this, this_disconnect_posts0, this_disconnect_posts0_rel + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this_disconnect_posts0, this_disconnect_posts0_rel + WITH collect(this_disconnect_posts0) as this_disconnect_posts0, this_disconnect_posts0_rel + UNWIND this_disconnect_posts0 as x + DELETE this_disconnect_posts0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_posts_Post + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -648,47 +648,47 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Comment\`) -WITH this -OPTIONAL MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) -CALL apoc.do.when(this_post0 IS NOT NULL, \\" -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_post_update_node_creator_disconnect_where_Userparam0 -WITH this, this_post0, this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel -CALL apoc.util.validate(NOT (any(auth_var1 IN [\\\\\\"super-admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\\\\\"admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\\\\\"@neo4j/graphql/FORBIDDEN\\\\\\", [0]) -CALL { - WITH this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel - WITH collect(this_post0_creator0_disconnect0) as this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel - UNWIND this_post0_creator0_disconnect0 as x - DELETE this_post0_creator0_disconnect0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_post0_creator0_disconnect_User -} -WITH this, this_post0 -CALL { - WITH this_post0 - MATCH (this_post0)-[this_post0_creator_User_unique:HAS_POST]->(:User) - WITH count(this_post0_creator_User_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDPost.creator required', [0]) - RETURN c AS this_post0_creator_User_unique_ignored -} -RETURN count(*) AS _ -\\", \\"\\", {this:this, updateComments: $updateComments, this_post0:this_post0, auth:$auth,updateComments_args_update_post_update_node_creator_disconnect_where_Userparam0:$updateComments_args_update_post_update_node_creator_disconnect_where_Userparam0}) -YIELD value AS _ -WITH this -CALL { - WITH this - MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) - WITH count(this_post_Post_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) - RETURN c AS this_post_Post_unique_ignored -} -RETURN collect(DISTINCT this { .content }) AS data" -`); + "MATCH (this:\`Comment\`) + WITH this + OPTIONAL MATCH (this)<-[this_has_comment0_relationship:HAS_COMMENT]-(this_post0:Post) + CALL apoc.do.when(this_post0 IS NOT NULL, \\" + 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_post_update_node_creator_disconnect_where_Userparam0 + WITH this, this_post0, this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\\\\\"super-admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1)) AND any(auth_var1 IN [\\\\\\"admin\\\\\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\\\\\"@neo4j/graphql/FORBIDDEN\\\\\\", [0]) + CALL { + WITH this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel + WITH collect(this_post0_creator0_disconnect0) as this_post0_creator0_disconnect0, this_post0_creator0_disconnect0_rel + UNWIND this_post0_creator0_disconnect0 as x + DELETE this_post0_creator0_disconnect0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_post0_creator0_disconnect_User + } + WITH this, this_post0 + CALL { + WITH this_post0 + MATCH (this_post0)-[this_post0_creator_User_unique:HAS_POST]->(:User) + WITH count(this_post0_creator_User_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDPost.creator required', [0]) + RETURN c AS this_post0_creator_User_unique_ignored + } + RETURN count(*) AS _ + \\", \\"\\", {this:this, updateComments: $updateComments, this_post0:this_post0, auth:$auth,updateComments_args_update_post_update_node_creator_disconnect_where_Userparam0:$updateComments_args_update_post_update_node_creator_disconnect_where_Userparam0}) + YIELD value AS _ + WITH this + CALL { + WITH this + MATCH (this)<-[this_post_Post_unique:HAS_COMMENT]-(:Post) + WITH count(this_post_Post_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDComment.post required', [0]) + RETURN c AS this_post_Post_unique_ignored + } + RETURN collect(DISTINCT this { .content }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -785,22 +785,22 @@ RETURN collect(DISTINCT this { .content }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`User\`) -WITH this -OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) -WITH this, this_posts0 -CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete -CALL { - WITH this_posts0_to_delete - UNWIND this_posts0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this -CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) -DETACH DELETE this" -`); + "MATCH (this:\`User\`) + WITH this + OPTIONAL MATCH (this)-[this_posts0_relationship:HAS_POST]->(this_posts0:Post) + WITH this, this_posts0 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"super-admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this, collect(DISTINCT this_posts0) as this_posts0_to_delete + CALL { + WITH this_posts0_to_delete + UNWIND this_posts0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/autogenerate.test.ts b/packages/graphql/tests/tck/directives/autogenerate.test.ts index 570975af3c..2fa49ecf30 100644 --- a/packages/graphql/tests/tck/directives/autogenerate.test.ts +++ b/packages/graphql/tests/tck/directives/autogenerate.test.ts @@ -59,19 +59,25 @@ describe("Cypher autogenerate directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = randomUUID() - SET this0.name = $this0_name - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.name = create_var1.name, + create_this0.id = randomUUID() + RETURN create_this0 } - RETURN [ - this0 { .id, .name }] AS data" + RETURN collect(create_this0 { .id, .name }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_name\\": \\"dan\\", + \\"create_param0\\": [ + { + \\"name\\": \\"dan\\" + } + ], \\"resolvedCallbacks\\": {} }" `); 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 5a22f45726..9cbe2688ac 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 @@ -129,35 +129,56 @@ describe("Node directive with additionalLabels", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:\`Film\`:\`Multimedia\`) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_actors0_node:\`Actor\`:\`Person\`) - SET this0_actors0_node.name = $this0_actors0_node_name - MERGE (this0)<-[:ACTED_IN]-(this0_actors0_node) - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:\`Film\`:\`Multimedia\`) - SET this1.id = $this1_id - WITH this1 - CREATE (this1_actors0_node:\`Actor\`:\`Person\`) - SET this1_actors0_node.name = $this1_actors0_node_name - MERGE (this1)<-[:ACTED_IN]-(this1_actors0_node) - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Film\`:\`Multimedia\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`:\`Person\`) + SET + create_this5.name = create_var3.name + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + RETURN collect(NULL) + } + RETURN create_this0 } - RETURN [ - this0 { .id }, - this1 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this0_actors0_node_name\\": \\"actor 1\\", - \\"this1_id\\": \\"2\\", - \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 2\\" + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/node/node-label-jwt.test.ts b/packages/graphql/tests/tck/directives/node/node-label-jwt.test.ts index bb8fc9bb92..bef37f6930 100644 --- a/packages/graphql/tests/tck/directives/node/node-label-jwt.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-label-jwt.test.ts @@ -135,18 +135,24 @@ describe("Label in Node directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:\`Film\`) - SET this0.title = $this0_title - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Film\`) + SET + create_this0.title = create_var1.title + RETURN create_this0 } - RETURN [ - this0 { .title }] AS data" + RETURN collect(create_this0 { .title }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_title\\": \\"Titanic\\", + \\"create_param0\\": [ + { + \\"title\\": \\"Titanic\\" + } + ], \\"resolvedCallbacks\\": {} }" `); 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 497a3eb4a7..dbfdc3999e 100644 --- a/packages/graphql/tests/tck/directives/node/node-label.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-label.test.ts @@ -161,18 +161,24 @@ describe("Label in Node directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:\`Film\`) - SET this0.id = $this0_id - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Film\`) + SET + create_this0.id = create_var1.id + RETURN create_this0 } - RETURN [ - this0 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -200,35 +206,56 @@ describe("Label in Node directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:\`Film\`) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_actors0_node:\`Person\`) - SET this0_actors0_node.name = $this0_actors0_node_name - MERGE (this0)<-[:ACTED_IN]-(this0_actors0_node) - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:\`Film\`) - SET this1.id = $this1_id - WITH this1 - CREATE (this1_actors0_node:\`Person\`) - SET this1_actors0_node.name = $this1_actors0_node_name - MERGE (this1)<-[:ACTED_IN]-(this1_actors0_node) - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Film\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Person\`) + SET + create_this5.name = create_var3.name + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + RETURN collect(NULL) + } + RETURN create_this0 } - RETURN [ - this0 { .id }, - this1 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this0_actors0_node_name\\": \\"actor 1\\", - \\"this1_id\\": \\"2\\", - \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 2\\" + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -355,26 +382,26 @@ describe("Label in Node directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Film\`) -WHERE this.id = $param0 -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_actors0_node:\`Person\`) - WHERE this_connect_actors0_node.name = $this_connect_actors0_node_param0 - CALL { - WITH * - WITH collect(this_connect_actors0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_actors0_node - MERGE (this)<-[:ACTED_IN]-(this_connect_actors0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_actors_Actor -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`Film\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_actors0_node:\`Person\`) + WHERE this_connect_actors0_node.name = $this_connect_actors0_node_param0 + CALL { + WITH * + WITH collect(this_connect_actors0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_actors0_node + MERGE (this)<-[:ACTED_IN]-(this_connect_actors0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_actors_Actor + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -402,25 +429,25 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Film\`) -WHERE this.id = $param0 -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_disconnect_actors0_rel:ACTED_IN]-(this_disconnect_actors0:\`Person\`) -WHERE this_disconnect_actors0.name = $updateMovies_args_disconnect_actors0_where_Actorparam0 -CALL { - WITH this_disconnect_actors0, this_disconnect_actors0_rel - WITH collect(this_disconnect_actors0) as this_disconnect_actors0, this_disconnect_actors0_rel - UNWIND this_disconnect_actors0 as x - DELETE this_disconnect_actors0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_actors_Actor -} -WITH * -RETURN collect(DISTINCT this { .id }) AS data" -`); + "MATCH (this:\`Film\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_disconnect_actors0_rel:ACTED_IN]-(this_disconnect_actors0:\`Person\`) + WHERE this_disconnect_actors0.name = $updateMovies_args_disconnect_actors0_where_Actorparam0 + CALL { + WITH this_disconnect_actors0, this_disconnect_actors0_rel + WITH collect(this_disconnect_actors0) as this_disconnect_actors0, this_disconnect_actors0_rel + UNWIND this_disconnect_actors0 as x + DELETE this_disconnect_actors0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_actors_Actor + } + WITH * + RETURN collect(DISTINCT this { .id }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -488,20 +515,20 @@ RETURN collect(DISTINCT this { .id }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Film\`) -WHERE this.id = $param0 -WITH this -OPTIONAL MATCH (this)<-[this_actors0_relationship:ACTED_IN]-(this_actors0:\`Person\`) -WHERE this_actors0.name = $this_deleteMovies_args_delete_actors0_where_Actorparam0 -WITH this, collect(DISTINCT this_actors0) as this_actors0_to_delete -CALL { - WITH this_actors0_to_delete - UNWIND this_actors0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -DETACH DELETE this" -`); + "MATCH (this:\`Film\`) + WHERE this.id = $param0 + WITH this + OPTIONAL MATCH (this)<-[this_actors0_relationship:ACTED_IN]-(this_actors0:\`Person\`) + WHERE this_actors0.name = $this_deleteMovies_args_delete_actors0_where_Actorparam0 + WITH this, collect(DISTINCT this_actors0) as this_actors0_to_delete + CALL { + WITH this_actors0_to_delete + UNWIND this_actors0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + DETACH DELETE this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/directives/plural.test.ts b/packages/graphql/tests/tck/directives/plural.test.ts index bc7c0434c9..9e593d5269 100644 --- a/packages/graphql/tests/tck/directives/plural.test.ts +++ b/packages/graphql/tests/tck/directives/plural.test.ts @@ -108,18 +108,24 @@ describe("Plural directive", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Tech) - SET this0.name = $this0_name - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Tech\`) + SET + create_this0.name = create_var1.name + RETURN create_this0 } - RETURN [ - this0 { .name }] AS data" + RETURN collect(create_this0 { .name }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_name\\": \\"Highlander\\", + \\"create_param0\\": [ + { + \\"name\\": \\"Highlander\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/timestamp/datetime.test.ts b/packages/graphql/tests/tck/directives/timestamp/datetime.test.ts index 9c9266deff..f1102770bd 100644 --- a/packages/graphql/tests/tck/directives/timestamp/datetime.test.ts +++ b/packages/graphql/tests/tck/directives/timestamp/datetime.test.ts @@ -67,21 +67,27 @@ describe("Cypher TimeStamps On DateTime Fields", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.createdAt = datetime() - SET this0.interfaceTimestamp = datetime() - SET this0.overrideTimestamp = datetime() - SET this0.id = $this0_id - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id, + create_this0.createdAt = datetime(), + create_this0.interfaceTimestamp = datetime(), + create_this0.overrideTimestamp = datetime() + RETURN create_this0 } - RETURN [ - this0 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"123\\", + \\"create_param0\\": [ + { + \\"id\\": \\"123\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/timestamp/time.test.ts b/packages/graphql/tests/tck/directives/timestamp/time.test.ts index 8abb62aada..9f3c988c1c 100644 --- a/packages/graphql/tests/tck/directives/timestamp/time.test.ts +++ b/packages/graphql/tests/tck/directives/timestamp/time.test.ts @@ -67,21 +67,27 @@ describe("Cypher TimeStamps On Time Fields", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.createdAt = time() - SET this0.interfaceTimestamp = time() - SET this0.overrideTimestamp = time() - SET this0.id = $this0_id - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id, + create_this0.createdAt = time(), + create_this0.interfaceTimestamp = time(), + create_this0.overrideTimestamp = time() + RETURN create_this0 } - RETURN [ - this0 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"123\\", + \\"create_param0\\": [ + { + \\"id\\": \\"123\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/issues/288.test.ts b/packages/graphql/tests/tck/issues/288.test.ts index 9c755d23a5..76f8fd4c51 100644 --- a/packages/graphql/tests/tck/issues/288.test.ts +++ b/packages/graphql/tests/tck/issues/288.test.ts @@ -64,20 +64,26 @@ describe("#288", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:USER) - SET this0.USERID = $this0_USERID - SET this0.COMPANYID = $this0_COMPANYID - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`USER\`) + SET + create_this0.USERID = create_var1.USERID, + create_this0.COMPANYID = create_var1.COMPANYID + RETURN create_this0 } - RETURN [ - this0 { .USERID, .COMPANYID }] AS data" + RETURN collect(create_this0 { .USERID, .COMPANYID }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_USERID\\": \\"userid\\", - \\"this0_COMPANYID\\": \\"companyid\\", + \\"create_param0\\": [ + { + \\"USERID\\": \\"userid\\", + \\"COMPANYID\\": \\"companyid\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts b/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts new file mode 100644 index 0000000000..a829b94a03 --- /dev/null +++ b/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts @@ -0,0 +1,550 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; +import { gql } from "apollo-server"; +import type { DocumentNode } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { createJwtRequest } from "../../../utils/create-jwt-request"; +import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils"; + +describe("Batch Create, Auth", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type Actor @auth(rules: [{ allow: { id: "$jwt.sub" } }]) { + id: ID! @id + name: String + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type Movie @auth(rules: [{ operations: [CREATE, UPDATE], roles: ["admin"] }]) { + id: ID + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Website { + address: String + } + + interface ActedIn @relationshipProperties { + year: Int + } + `; + const secret = "secret"; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { enableRegex: true }, + plugins: { + auth: new Neo4jGraphQLAuthJWTPlugin({ + secret, + }), + }, + }); + }); + + test("no nested batch", async () => { + const query = gql` + mutation { + createMovies(input: [{ id: "1" }, { id: "2" }]) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + RETURN [ + this0 { .id }, + this1 { .id }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this1_id\\": \\"2\\", + \\"resolvedCallbacks\\": {}, + \\"auth\\": { + \\"isAuthenticated\\": true, + \\"roles\\": [], + \\"jwt\\": { + \\"roles\\": [], + \\"sub\\": \\"1\\" + } + } + }" + `); + }); + + test("nested batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 2" }, edge: { year: 2022 } }] } } + ] + ) { + movies { + id + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CREATE (this0_actors0_node:Actor) + SET this0_actors0_node.id = randomUUID() + SET this0_actors0_node.name = $this0_actors0_node_name + MERGE (this0)<-[this0_actors0_relationship:ACTED_IN]-(this0_actors0_node) + SET this0_actors0_relationship.year = $this0_actors0_relationship_year + WITH this0, this0_actors0_node + CALL { + WITH this0_actors0_node + MATCH (this0_actors0_node)-[this0_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_actors0_node_website_Website_unique_ignored + } + WITH this0 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CREATE (this1_actors0_node:Actor) + SET this1_actors0_node.id = randomUUID() + SET this1_actors0_node.name = $this1_actors0_node_name + MERGE (this1)<-[this1_actors0_relationship:ACTED_IN]-(this1_actors0_node) + SET this1_actors0_relationship.year = $this1_actors0_relationship_year + WITH this1, this1_actors0_node + CALL { + WITH this1_actors0_node + MATCH (this1_actors0_node)-[this1_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this1_actors0_node_website_Website_unique_ignored + } + WITH this1 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + WITH this0 + MATCH (this0_actors:\`Actor\`)-[create_this0:ACTED_IN]->(this0) + WHERE apoc.util.validatePredicate(NOT ((this0_actors.id IS NOT NULL AND this0_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this0_actors { .name } AS this0_actors + RETURN collect(this0_actors) AS this0_actors + } + CALL { + WITH this1 + MATCH (this1_actors:\`Actor\`)-[create_this0:ACTED_IN]->(this1) + WHERE apoc.util.validatePredicate(NOT ((this1_actors.id IS NOT NULL AND this1_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this1_actors { .name } AS this1_actors + RETURN collect(this1_actors) AS this1_actors + } + RETURN [ + this0 { .id, actors: this0_actors }, + this1 { .id, actors: this1_actors }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": \\"1\\", + \\"this0_id\\": \\"1\\", + \\"this0_actors0_node_name\\": \\"actor 1\\", + \\"this0_actors0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"this1_actors0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"resolvedCallbacks\\": {}, + \\"auth\\": { + \\"isAuthenticated\\": true, + \\"roles\\": [], + \\"jwt\\": { + \\"roles\\": [], + \\"sub\\": \\"1\\" + } + } + }" + `); + }); + + test("heterogeneous batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 2" }, edge: { year: 1999 } }] } } + { id: "3", website: { create: { node: { address: "mywebsite.com" } } } } + { id: "4", actors: { connect: { where: { node: { id: "2" } } } } } + { + id: "5" + actors: { + connectOrCreate: { + where: { node: { id: "2" } } + onCreate: { node: { name: "actor 2" } } + } + } + } + ] + ) { + movies { + id + website { + address + } + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CREATE (this0_actors0_node:Actor) + SET this0_actors0_node.id = randomUUID() + SET this0_actors0_node.name = $this0_actors0_node_name + MERGE (this0)<-[this0_actors0_relationship:ACTED_IN]-(this0_actors0_node) + SET this0_actors0_relationship.year = $this0_actors0_relationship_year + WITH this0, this0_actors0_node + CALL { + WITH this0_actors0_node + MATCH (this0_actors0_node)-[this0_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_actors0_node_website_Website_unique_ignored + } + WITH this0 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CREATE (this1_actors0_node:Actor) + SET this1_actors0_node.id = randomUUID() + SET this1_actors0_node.name = $this1_actors0_node_name + MERGE (this1)<-[this1_actors0_relationship:ACTED_IN]-(this1_actors0_node) + SET this1_actors0_relationship.year = $this1_actors0_relationship_year + WITH this1, this1_actors0_node + CALL { + WITH this1_actors0_node + MATCH (this1_actors0_node)-[this1_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this1_actors0_node_website_Website_unique_ignored + } + WITH this1 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + CREATE (this2:Movie) + SET this2.id = $this2_id + WITH this2 + CREATE (this2_website0_node:Website) + SET this2_website0_node.address = $this2_website0_node_address + MERGE (this2)-[:HAS_WEBSITE]->(this2_website0_node) + WITH this2 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this2 + CALL { + WITH this2 + MATCH (this2)-[this2_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this2_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this2_website_Website_unique_ignored + } + RETURN this2 + } + CALL { + CREATE (this3:Movie) + SET this3.id = $this3_id + WITH this3 + CALL { + WITH this3 + OPTIONAL MATCH (this3_actors_connect0_node:Actor) + WHERE this3_actors_connect0_node.id = $this3_actors_connect0_node_param0 + WITH this3, this3_actors_connect0_node + CALL apoc.util.validate(NOT ((this3_actors_connect0_node.id IS NOT NULL AND this3_actors_connect0_node.id = $this3_actors_connect0_nodeauth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH * + WITH collect(this3_actors_connect0_node) as connectedNodes, collect(this3) as parentNodes + UNWIND parentNodes as this3 + UNWIND connectedNodes as this3_actors_connect0_node + MERGE (this3)<-[this3_actors_connect0_relationship:ACTED_IN]-(this3_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this3_actors_connect_Actor + } + WITH this3 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this3 + CALL { + WITH this3 + MATCH (this3)-[this3_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this3_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this3_website_Website_unique_ignored + } + RETURN this3 + } + CALL { + CREATE (this4:Movie) + SET this4.id = $this4_id + WITH this4 + CALL { + WITH this4 + MERGE (this4_actors_connectOrCreate0:\`Actor\` { id: $this4_actors_connectOrCreate_param0 }) + ON CREATE SET + this4_actors_connectOrCreate0.name = $this4_actors_connectOrCreate_param1 + MERGE (this4_actors_connectOrCreate0)-[this4_actors_connectOrCreate_this0:ACTED_IN]->(this4) + WITH * + CALL apoc.util.validate(NOT ((this4_actors_connectOrCreate0.id IS NOT NULL AND this4_actors_connectOrCreate0.id = $this4_actors_connectOrCreate0auth_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN COUNT(*) AS _ + } + WITH this4 + CALL apoc.util.validate(NOT (any(auth_var1 IN [\\"admin\\"] WHERE any(auth_var0 IN $auth.roles WHERE auth_var0 = auth_var1))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this4 + CALL { + WITH this4 + MATCH (this4)-[this4_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this4_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this4_website_Website_unique_ignored + } + RETURN this4 + } + CALL { + WITH this0 + MATCH (this0)-[create_this0:HAS_WEBSITE]->(this0_website:\`Website\`) + WITH this0_website { .address } AS this0_website + RETURN head(collect(this0_website)) AS this0_website + } + CALL { + WITH this0 + MATCH (this0_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this0) + WHERE apoc.util.validatePredicate(NOT ((this0_actors.id IS NOT NULL AND this0_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this0_actors { .name } AS this0_actors + RETURN collect(this0_actors) AS this0_actors + } + CALL { + WITH this1 + MATCH (this1)-[create_this0:HAS_WEBSITE]->(this1_website:\`Website\`) + WITH this1_website { .address } AS this1_website + RETURN head(collect(this1_website)) AS this1_website + } + CALL { + WITH this1 + MATCH (this1_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this1) + WHERE apoc.util.validatePredicate(NOT ((this1_actors.id IS NOT NULL AND this1_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this1_actors { .name } AS this1_actors + RETURN collect(this1_actors) AS this1_actors + } + CALL { + WITH this2 + MATCH (this2)-[create_this0:HAS_WEBSITE]->(this2_website:\`Website\`) + WITH this2_website { .address } AS this2_website + RETURN head(collect(this2_website)) AS this2_website + } + CALL { + WITH this2 + MATCH (this2_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this2) + WHERE apoc.util.validatePredicate(NOT ((this2_actors.id IS NOT NULL AND this2_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this2_actors { .name } AS this2_actors + RETURN collect(this2_actors) AS this2_actors + } + CALL { + WITH this3 + MATCH (this3)-[create_this0:HAS_WEBSITE]->(this3_website:\`Website\`) + WITH this3_website { .address } AS this3_website + RETURN head(collect(this3_website)) AS this3_website + } + CALL { + WITH this3 + MATCH (this3_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this3) + WHERE apoc.util.validatePredicate(NOT ((this3_actors.id IS NOT NULL AND this3_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this3_actors { .name } AS this3_actors + RETURN collect(this3_actors) AS this3_actors + } + CALL { + WITH this4 + MATCH (this4)-[create_this0:HAS_WEBSITE]->(this4_website:\`Website\`) + WITH this4_website { .address } AS this4_website + RETURN head(collect(this4_website)) AS this4_website + } + CALL { + WITH this4 + MATCH (this4_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this4) + WHERE apoc.util.validatePredicate(NOT ((this4_actors.id IS NOT NULL AND this4_actors.id = $create_param0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH this4_actors { .name } AS this4_actors + RETURN collect(this4_actors) AS this4_actors + } + RETURN [ + this0 { .id, website: this0_website, actors: this0_actors }, + this1 { .id, website: this1_website, actors: this1_actors }, + this2 { .id, website: this2_website, actors: this2_actors }, + this3 { .id, website: this3_website, actors: this3_actors }, + this4 { .id, website: this4_website, actors: this4_actors }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": \\"1\\", + \\"this0_id\\": \\"1\\", + \\"this0_actors0_node_name\\": \\"actor 1\\", + \\"this0_actors0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"this1_actors0_relationship_year\\": { + \\"low\\": 1999, + \\"high\\": 0 + }, + \\"this2_id\\": \\"3\\", + \\"this2_website0_node_address\\": \\"mywebsite.com\\", + \\"this3_id\\": \\"4\\", + \\"this3_actors_connect0_node_param0\\": \\"2\\", + \\"this3_actors_connect0_nodeauth_param0\\": \\"1\\", + \\"this4_id\\": \\"5\\", + \\"this4_actors_connectOrCreate_param0\\": \\"2\\", + \\"this4_actors_connectOrCreate_param1\\": \\"actor 2\\", + \\"this4_actors_connectOrCreate0auth_param0\\": \\"1\\", + \\"resolvedCallbacks\\": {}, + \\"auth\\": { + \\"isAuthenticated\\": true, + \\"roles\\": [], + \\"jwt\\": { + \\"roles\\": [], + \\"sub\\": \\"1\\" + } + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/operations/batch/batch-create-fields.test.ts b/packages/graphql/tests/tck/operations/batch/batch-create-fields.test.ts new file mode 100644 index 0000000000..e51af98d7e --- /dev/null +++ b/packages/graphql/tests/tck/operations/batch/batch-create-fields.test.ts @@ -0,0 +1,666 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "apollo-server"; +import type { DocumentNode } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { createJwtRequest } from "../../../utils/create-jwt-request"; +import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils"; + +describe("Batch Create, Scalar types", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type Actor { + id: ID! @id + name: String + born: Date + createdAt: DateTime @timestamp(operations: [CREATE]) + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type Movie { + id: ID + runningTime: Duration + location: Point + createdAt: DateTime @timestamp(operations: [CREATE]) + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Website { + address: String + } + + interface ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { enableRegex: true }, + }); + }); + + test("no nested batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", runningTime: "P14DT16H12M", location: { longitude: 3.0, latitude: 3.0 } } + { id: "2" } + ] + ) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", {}); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id, + create_this0.runningTime = create_var1.runningTime, + create_this0.location = point(create_var1.location), + create_this0.createdAt = datetime() + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 + } + RETURN collect(create_this0 { .id }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"runningTime\\": { + \\"months\\": 0, + \\"days\\": 14, + \\"seconds\\": { + \\"low\\": 58320, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + }, + \\"location\\": { + \\"longitude\\": 3, + \\"latitude\\": 3 + } + }, + { + \\"id\\": \\"2\\" + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("1 to 1 cardinality", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { + id: "1" + actors: { + create: [ + { + node: { + name: "actor 1" + website: { create: { node: { address: "Actor1.com" } } } + } + edge: { year: 2022 } + } + ] + } + } + { id: "2", website: { create: { node: { address: "The Matrix2.com" } } } } + ] + ) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", {}); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id, + create_this0.createdAt = datetime() + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`) + SET + create_this5.name = create_var3.name, + create_this5.createdAt = datetime(), + create_this5.id = randomUUID() + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + SET + create_this6.year = create_var4.year + WITH create_this5, create_var3 + CALL { + WITH create_this5, create_var3 + UNWIND create_var3.website.create AS create_var7 + WITH create_var7.node AS create_var8, create_var7.edge AS create_var9, create_this5 + CREATE (create_this10:\`Website\`) + SET + create_this10.address = create_var8.address + MERGE (create_this5)-[create_this11:HAS_WEBSITE]->(create_this10) + RETURN collect(NULL) + } + WITH create_this5 + CALL { + WITH create_this5 + MATCH (create_this5)-[create_this5_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this5_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS create_this5_website_Website_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.website.create AS create_var12 + WITH create_var12.node AS create_var13, create_var12.edge AS create_var14, create_this0 + CREATE (create_this15:\`Website\`) + SET + create_this15.address = create_var13.address + MERGE (create_this0)-[create_this16:HAS_WEBSITE]->(create_this15) + RETURN collect(NULL) + } + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 + } + RETURN collect(create_this0 { .id }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\", + \\"website\\": { + \\"create\\": { + \\"node\\": { + \\"address\\": \\"Actor1.com\\" + } + } + } + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"website\\": { + \\"create\\": { + \\"node\\": { + \\"address\\": \\"The Matrix2.com\\" + } + } + } + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("nested batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + ] + ) { + movies { + id + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", {}); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var2 + CALL { + WITH create_var2 + CREATE (create_this1:\`Movie\`) + SET + create_this1.id = create_var2.id, + create_this1.createdAt = datetime() + WITH create_this1, create_var2 + CALL { + WITH create_this1, create_var2 + UNWIND create_var2.actors.create AS create_var3 + WITH create_var3.node AS create_var4, create_var3.edge AS create_var5, create_this1 + CREATE (create_this6:\`Actor\`) + SET + create_this6.name = create_var4.name, + create_this6.createdAt = datetime(), + create_this6.id = randomUUID() + MERGE (create_this6)-[create_this7:ACTED_IN]->(create_this1) + SET + create_this7.year = create_var5.year + WITH create_this6 + CALL { + WITH create_this6 + MATCH (create_this6)-[create_this6_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this6_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS create_this6_website_Website_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this1 + CALL { + WITH create_this1 + MATCH (create_this1)-[create_this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this1_website_Website_unique_ignored + } + RETURN create_this1 + } + CALL { + WITH create_this1 + MATCH (create_this1_actors:\`Actor\`)-[create_this0:ACTED_IN]->(create_this1) + WITH create_this1_actors { .name } AS create_this1_actors + RETURN collect(create_this1_actors) AS create_this1_actors + } + RETURN collect(create_this1 { .id, actors: create_this1_actors }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("heterogeneous batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 2" }, edge: { year: 1999 } }] } } + { id: "3", website: { create: { node: { address: "mywebsite.com" } } } } + { id: "4", actors: { connect: { where: { node: { id: "2" } } } } } + { + id: "5" + actors: { + connectOrCreate: { + where: { node: { id: "2" } } + onCreate: { node: { name: "actor 2" } } + } + } + } + ] + ) { + movies { + id + website { + address + } + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", {}); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.createdAt = datetime() + SET this0.id = $this0_id + WITH this0 + CREATE (this0_actors0_node:Actor) + SET this0_actors0_node.createdAt = datetime() + SET this0_actors0_node.id = randomUUID() + SET this0_actors0_node.name = $this0_actors0_node_name + MERGE (this0)<-[this0_actors0_relationship:ACTED_IN]-(this0_actors0_node) + SET this0_actors0_relationship.year = $this0_actors0_relationship_year + WITH this0, this0_actors0_node + CALL { + WITH this0_actors0_node + MATCH (this0_actors0_node)-[this0_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_actors0_node_website_Website_unique_ignored + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.createdAt = datetime() + SET this1.id = $this1_id + WITH this1 + CREATE (this1_actors0_node:Actor) + SET this1_actors0_node.createdAt = datetime() + SET this1_actors0_node.id = randomUUID() + SET this1_actors0_node.name = $this1_actors0_node_name + MERGE (this1)<-[this1_actors0_relationship:ACTED_IN]-(this1_actors0_node) + SET this1_actors0_relationship.year = $this1_actors0_relationship_year + WITH this1, this1_actors0_node + CALL { + WITH this1_actors0_node + MATCH (this1_actors0_node)-[this1_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this1_actors0_node_website_Website_unique_ignored + } + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + CREATE (this2:Movie) + SET this2.createdAt = datetime() + SET this2.id = $this2_id + WITH this2 + CREATE (this2_website0_node:Website) + SET this2_website0_node.address = $this2_website0_node_address + MERGE (this2)-[:HAS_WEBSITE]->(this2_website0_node) + WITH this2 + CALL { + WITH this2 + MATCH (this2)-[this2_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this2_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this2_website_Website_unique_ignored + } + RETURN this2 + } + CALL { + CREATE (this3:Movie) + SET this3.createdAt = datetime() + SET this3.id = $this3_id + WITH this3 + CALL { + WITH this3 + OPTIONAL MATCH (this3_actors_connect0_node:Actor) + WHERE this3_actors_connect0_node.id = $this3_actors_connect0_node_param0 + CALL { + WITH * + WITH collect(this3_actors_connect0_node) as connectedNodes, collect(this3) as parentNodes + UNWIND parentNodes as this3 + UNWIND connectedNodes as this3_actors_connect0_node + MERGE (this3)<-[this3_actors_connect0_relationship:ACTED_IN]-(this3_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this3_actors_connect_Actor + } + WITH this3 + CALL { + WITH this3 + MATCH (this3)-[this3_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this3_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this3_website_Website_unique_ignored + } + RETURN this3 + } + CALL { + CREATE (this4:Movie) + SET this4.createdAt = datetime() + SET this4.id = $this4_id + WITH this4 + CALL { + WITH this4 + MERGE (this4_actors_connectOrCreate0:\`Actor\` { id: $this4_actors_connectOrCreate_param0 }) + ON CREATE SET + this4_actors_connectOrCreate0.createdAt = datetime(), + this4_actors_connectOrCreate0.name = $this4_actors_connectOrCreate_param1 + MERGE (this4_actors_connectOrCreate0)-[this4_actors_connectOrCreate_this0:ACTED_IN]->(this4) + RETURN COUNT(*) AS _ + } + WITH this4 + CALL { + WITH this4 + MATCH (this4)-[this4_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this4_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this4_website_Website_unique_ignored + } + RETURN this4 + } + CALL { + WITH this0 + MATCH (this0)-[create_this0:HAS_WEBSITE]->(this0_website:\`Website\`) + WITH this0_website { .address } AS this0_website + RETURN head(collect(this0_website)) AS this0_website + } + CALL { + WITH this0 + MATCH (this0_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this0) + WITH this0_actors { .name } AS this0_actors + RETURN collect(this0_actors) AS this0_actors + } + CALL { + WITH this1 + MATCH (this1)-[create_this0:HAS_WEBSITE]->(this1_website:\`Website\`) + WITH this1_website { .address } AS this1_website + RETURN head(collect(this1_website)) AS this1_website + } + CALL { + WITH this1 + MATCH (this1_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this1) + WITH this1_actors { .name } AS this1_actors + RETURN collect(this1_actors) AS this1_actors + } + CALL { + WITH this2 + MATCH (this2)-[create_this0:HAS_WEBSITE]->(this2_website:\`Website\`) + WITH this2_website { .address } AS this2_website + RETURN head(collect(this2_website)) AS this2_website + } + CALL { + WITH this2 + MATCH (this2_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this2) + WITH this2_actors { .name } AS this2_actors + RETURN collect(this2_actors) AS this2_actors + } + CALL { + WITH this3 + MATCH (this3)-[create_this0:HAS_WEBSITE]->(this3_website:\`Website\`) + WITH this3_website { .address } AS this3_website + RETURN head(collect(this3_website)) AS this3_website + } + CALL { + WITH this3 + MATCH (this3_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this3) + WITH this3_actors { .name } AS this3_actors + RETURN collect(this3_actors) AS this3_actors + } + CALL { + WITH this4 + MATCH (this4)-[create_this0:HAS_WEBSITE]->(this4_website:\`Website\`) + WITH this4_website { .address } AS this4_website + RETURN head(collect(this4_website)) AS this4_website + } + CALL { + WITH this4 + MATCH (this4_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this4) + WITH this4_actors { .name } AS this4_actors + RETURN collect(this4_actors) AS this4_actors + } + RETURN [ + this0 { .id, website: this0_website, actors: this0_actors }, + this1 { .id, website: this1_website, actors: this1_actors }, + this2 { .id, website: this2_website, actors: this2_actors }, + this3 { .id, website: this3_website, actors: this3_actors }, + this4 { .id, website: this4_website, actors: this4_actors }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this0_actors0_node_name\\": \\"actor 1\\", + \\"this0_actors0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"this1_actors0_relationship_year\\": { + \\"low\\": 1999, + \\"high\\": 0 + }, + \\"this2_id\\": \\"3\\", + \\"this2_website0_node_address\\": \\"mywebsite.com\\", + \\"this3_id\\": \\"4\\", + \\"this3_actors_connect0_node_param0\\": \\"2\\", + \\"this4_id\\": \\"5\\", + \\"this4_actors_connectOrCreate_param0\\": \\"2\\", + \\"this4_actors_connectOrCreate_param1\\": \\"actor 2\\", + \\"resolvedCallbacks\\": {} + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/operations/batch/batch-create-interface.test.ts b/packages/graphql/tests/tck/operations/batch/batch-create-interface.test.ts new file mode 100644 index 0000000000..527d1ec2ec --- /dev/null +++ b/packages/graphql/tests/tck/operations/batch/batch-create-interface.test.ts @@ -0,0 +1,531 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "apollo-server"; +import type { DocumentNode } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { createJwtRequest } from "../../../utils/create-jwt-request"; +import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils"; + +describe("Batch Create, Interface", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + interface Person { + id: ID! + name: String + } + + type Actor implements Person { + id: ID! + name: String + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "EMPLOYED", direction: OUT, properties: "ActedIn") + } + + type Modeler implements Person { + id: ID! + name: String + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "EMPLOYED", direction: OUT, properties: "ActedIn") + } + + type Movie { + id: ID + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + workers: [Person!]! @relationship(type: "EMPLOYED", direction: IN, properties: "ActedIn") + } + + type Website { + address: String + } + + interface ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { enableRegex: true }, + }); + }); + + test("no nested batch", async () => { + const query = gql` + mutation { + createMovies(input: [{ id: "1" }, { id: "2" }]) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 + } + RETURN collect(create_this0 { .id }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\" + }, + { + \\"id\\": \\"2\\" + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("nested batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { + id: "1" + workers: { + create: [{ node: { Actor: { id: "1", name: "actor 1" } }, edge: { year: 2022 } }] + } + } + { + id: "2" + workers: { + create: [{ node: { Modeler: { id: "2", name: "modeler 1" } }, edge: { year: 2022 } }] + } + } + ] + ) { + movies { + id + workers { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CREATE (this0_workersActor0_node:Actor) + SET this0_workersActor0_node.id = $this0_workersActor0_node_id + SET this0_workersActor0_node.name = $this0_workersActor0_node_name + MERGE (this0)<-[this0_workersActor0_relationship:EMPLOYED]-(this0_workersActor0_node) + SET this0_workersActor0_relationship.year = $this0_workersActor0_relationship_year + WITH this0, this0_workersActor0_node + CALL { + WITH this0_workersActor0_node + MATCH (this0_workersActor0_node)-[this0_workersActor0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_workersActor0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_workersActor0_node_website_Website_unique_ignored + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CREATE (this1_workersModeler0_node:Modeler) + SET this1_workersModeler0_node.id = $this1_workersModeler0_node_id + SET this1_workersModeler0_node.name = $this1_workersModeler0_node_name + MERGE (this1)<-[this1_workersModeler0_relationship:EMPLOYED]-(this1_workersModeler0_node) + SET this1_workersModeler0_relationship.year = $this1_workersModeler0_relationship_year + WITH this1, this1_workersModeler0_node + CALL { + WITH this1_workersModeler0_node + MATCH (this1_workersModeler0_node)-[this1_workersModeler0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_workersModeler0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDModeler.website must be less than or equal to one', [0]) + RETURN c AS this1_workersModeler0_node_website_Website_unique_ignored + } + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + WITH * + CALL { + WITH * + CALL { + WITH this0 + MATCH (this0)<-[create_this0:EMPLOYED]-(this0_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this0_Actor.name } AS this0_workers + UNION + WITH this0 + MATCH (this0)<-[create_this1:EMPLOYED]-(this0_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this0_Modeler.name } AS this0_workers + } + RETURN collect(this0_workers) AS this0_workers + } + WITH * + CALL { + WITH * + CALL { + WITH this1 + MATCH (this1)<-[create_this0:EMPLOYED]-(this1_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this1_Actor.name } AS this1_workers + UNION + WITH this1 + MATCH (this1)<-[create_this1:EMPLOYED]-(this1_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this1_Modeler.name } AS this1_workers + } + RETURN collect(this1_workers) AS this1_workers + } + RETURN [ + this0 { .id, workers: this0_workers }, + this1 { .id, workers: this1_workers }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this0_workersActor0_node_id\\": \\"1\\", + \\"this0_workersActor0_node_name\\": \\"actor 1\\", + \\"this0_workersActor0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_workersModeler0_node_id\\": \\"2\\", + \\"this1_workersModeler0_node_name\\": \\"modeler 1\\", + \\"this1_workersModeler0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("heterogeneous batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { + id: "1" + workers: { + create: [{ node: { Actor: { id: "1", name: "actor 1" } }, edge: { year: 2022 } }] + } + } + { + id: "2" + workers: { + create: [{ node: { Actor: { id: "2", name: "actor 2" } }, edge: { year: 2022 } }] + } + } + { id: "3", website: { create: { node: { address: "mywebsite.com" } } } } + { id: "4", workers: { connect: { where: { node: { id: "2" } } } } } + ] + ) { + movies { + id + website { + address + } + workers { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", { sub: "1" }); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CREATE (this0_workersActor0_node:Actor) + SET this0_workersActor0_node.id = $this0_workersActor0_node_id + SET this0_workersActor0_node.name = $this0_workersActor0_node_name + MERGE (this0)<-[this0_workersActor0_relationship:EMPLOYED]-(this0_workersActor0_node) + SET this0_workersActor0_relationship.year = $this0_workersActor0_relationship_year + WITH this0, this0_workersActor0_node + CALL { + WITH this0_workersActor0_node + MATCH (this0_workersActor0_node)-[this0_workersActor0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_workersActor0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_workersActor0_node_website_Website_unique_ignored + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CREATE (this1_workersActor0_node:Actor) + SET this1_workersActor0_node.id = $this1_workersActor0_node_id + SET this1_workersActor0_node.name = $this1_workersActor0_node_name + MERGE (this1)<-[this1_workersActor0_relationship:EMPLOYED]-(this1_workersActor0_node) + SET this1_workersActor0_relationship.year = $this1_workersActor0_relationship_year + WITH this1, this1_workersActor0_node + CALL { + WITH this1_workersActor0_node + MATCH (this1_workersActor0_node)-[this1_workersActor0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_workersActor0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this1_workersActor0_node_website_Website_unique_ignored + } + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + CREATE (this2:Movie) + SET this2.id = $this2_id + WITH this2 + CREATE (this2_website0_node:Website) + SET this2_website0_node.address = $this2_website0_node_address + MERGE (this2)-[:HAS_WEBSITE]->(this2_website0_node) + WITH this2 + CALL { + WITH this2 + MATCH (this2)-[this2_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this2_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this2_website_Website_unique_ignored + } + RETURN this2 + } + CALL { + CREATE (this3:Movie) + SET this3.id = $this3_id + WITH this3 + CALL { + WITH this3 + OPTIONAL MATCH (this3_workers_connect0_node:Actor) + WHERE this3_workers_connect0_node.id = $this3_workers_connect0_node_param0 + CALL { + WITH * + WITH collect(this3_workers_connect0_node) as connectedNodes, collect(this3) as parentNodes + UNWIND parentNodes as this3 + UNWIND connectedNodes as this3_workers_connect0_node + MERGE (this3)<-[this3_workers_connect0_relationship:EMPLOYED]-(this3_workers_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this3_workers_connect_Actor + } + CALL { + WITH this3 + OPTIONAL MATCH (this3_workers_connect0_node:Modeler) + WHERE this3_workers_connect0_node.id = $this3_workers_connect0_node_param0 + CALL { + WITH * + WITH collect(this3_workers_connect0_node) as connectedNodes, collect(this3) as parentNodes + UNWIND parentNodes as this3 + UNWIND connectedNodes as this3_workers_connect0_node + MERGE (this3)<-[this3_workers_connect0_relationship:EMPLOYED]-(this3_workers_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this3_workers_connect_Modeler + } + WITH this3 + CALL { + WITH this3 + MATCH (this3)-[this3_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this3_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this3_website_Website_unique_ignored + } + RETURN this3 + } + CALL { + WITH this0 + MATCH (this0)-[create_this0:HAS_WEBSITE]->(this0_website:\`Website\`) + WITH this0_website { .address } AS this0_website + RETURN head(collect(this0_website)) AS this0_website + } + WITH * + CALL { + WITH * + CALL { + WITH this0 + MATCH (this0)<-[create_this1:EMPLOYED]-(this0_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this0_Actor.name } AS this0_workers + UNION + WITH this0 + MATCH (this0)<-[create_this2:EMPLOYED]-(this0_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this0_Modeler.name } AS this0_workers + } + RETURN collect(this0_workers) AS this0_workers + } + CALL { + WITH this1 + MATCH (this1)-[create_this0:HAS_WEBSITE]->(this1_website:\`Website\`) + WITH this1_website { .address } AS this1_website + RETURN head(collect(this1_website)) AS this1_website + } + WITH * + CALL { + WITH * + CALL { + WITH this1 + MATCH (this1)<-[create_this1:EMPLOYED]-(this1_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this1_Actor.name } AS this1_workers + UNION + WITH this1 + MATCH (this1)<-[create_this2:EMPLOYED]-(this1_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this1_Modeler.name } AS this1_workers + } + RETURN collect(this1_workers) AS this1_workers + } + CALL { + WITH this2 + MATCH (this2)-[create_this0:HAS_WEBSITE]->(this2_website:\`Website\`) + WITH this2_website { .address } AS this2_website + RETURN head(collect(this2_website)) AS this2_website + } + WITH * + CALL { + WITH * + CALL { + WITH this2 + MATCH (this2)<-[create_this1:EMPLOYED]-(this2_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this2_Actor.name } AS this2_workers + UNION + WITH this2 + MATCH (this2)<-[create_this2:EMPLOYED]-(this2_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this2_Modeler.name } AS this2_workers + } + RETURN collect(this2_workers) AS this2_workers + } + CALL { + WITH this3 + MATCH (this3)-[create_this0:HAS_WEBSITE]->(this3_website:\`Website\`) + WITH this3_website { .address } AS this3_website + RETURN head(collect(this3_website)) AS this3_website + } + WITH * + CALL { + WITH * + CALL { + WITH this3 + MATCH (this3)<-[create_this1:EMPLOYED]-(this3_Actor:\`Actor\`) + RETURN { __resolveType: \\"Actor\\", name: this3_Actor.name } AS this3_workers + UNION + WITH this3 + MATCH (this3)<-[create_this2:EMPLOYED]-(this3_Modeler:\`Modeler\`) + RETURN { __resolveType: \\"Modeler\\", name: this3_Modeler.name } AS this3_workers + } + RETURN collect(this3_workers) AS this3_workers + } + RETURN [ + this0 { .id, website: this0_website, workers: this0_workers }, + this1 { .id, website: this1_website, workers: this1_workers }, + this2 { .id, website: this2_website, workers: this2_workers }, + this3 { .id, website: this3_website, workers: this3_workers }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this0_workersActor0_node_id\\": \\"1\\", + \\"this0_workersActor0_node_name\\": \\"actor 1\\", + \\"this0_workersActor0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_workersActor0_node_id\\": \\"2\\", + \\"this1_workersActor0_node_name\\": \\"actor 2\\", + \\"this1_workersActor0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this2_id\\": \\"3\\", + \\"this2_website0_node_address\\": \\"mywebsite.com\\", + \\"this3_id\\": \\"4\\", + \\"this3_workers_connect0_node_param0\\": \\"2\\", + \\"resolvedCallbacks\\": {} + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/operations/batch/batch-create.test.ts b/packages/graphql/tests/tck/operations/batch/batch-create.test.ts new file mode 100644 index 0000000000..375d6b0486 --- /dev/null +++ b/packages/graphql/tests/tck/operations/batch/batch-create.test.ts @@ -0,0 +1,734 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentNode } from "graphql"; +import { gql } from "apollo-server"; +import { Neo4jGraphQL } from "../../../../src"; +import { createJwtRequest } from "../../../utils/create-jwt-request"; +import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils"; + +describe("Batch Create", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type Actor { + id: ID! @id + name: String + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type Movie { + id: ID + website: Website @relationship(type: "HAS_WEBSITE", direction: OUT) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Website { + address: String + } + + interface ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { enableRegex: true }, + }); + }); + + test("no nested batch", async () => { + const query = gql` + mutation { + createMovies(input: [{ id: "1" }, { id: "2" }]) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", {}); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 + } + RETURN collect(create_this0 { .id }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\" + }, + { + \\"id\\": \\"2\\" + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("1 to 1 cardinality", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { + id: "1" + actors: { + create: [ + { + node: { + name: "actor 1" + website: { create: { node: { address: "Actor1.com" } } } + } + edge: { year: 2022 } + } + ] + } + } + { id: "2", website: { create: { node: { address: "The Matrix2.com" } } } } + ] + ) { + movies { + id + } + } + } + `; + + const req = createJwtRequest("secret", {}); + + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`) + SET + create_this5.name = create_var3.name, + create_this5.id = randomUUID() + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + SET + create_this6.year = create_var4.year + WITH create_this5, create_var3 + CALL { + WITH create_this5, create_var3 + UNWIND create_var3.website.create AS create_var7 + WITH create_var7.node AS create_var8, create_var7.edge AS create_var9, create_this5 + CREATE (create_this10:\`Website\`) + SET + create_this10.address = create_var8.address + MERGE (create_this5)-[create_this11:HAS_WEBSITE]->(create_this10) + RETURN collect(NULL) + } + WITH create_this5 + CALL { + WITH create_this5 + MATCH (create_this5)-[create_this5_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this5_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS create_this5_website_Website_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.website.create AS create_var12 + WITH create_var12.node AS create_var13, create_var12.edge AS create_var14, create_this0 + CREATE (create_this15:\`Website\`) + SET + create_this15.address = create_var13.address + MERGE (create_this0)-[create_this16:HAS_WEBSITE]->(create_this15) + RETURN collect(NULL) + } + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)-[create_this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this0_website_Website_unique_ignored + } + RETURN create_this0 + } + RETURN collect(create_this0 { .id }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\", + \\"website\\": { + \\"create\\": { + \\"node\\": { + \\"address\\": \\"Actor1.com\\" + } + } + } + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"website\\": { + \\"create\\": { + \\"node\\": { + \\"address\\": \\"The Matrix2.com\\" + } + } + } + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("nested batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + ] + ) { + movies { + id + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", {}); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $create_param0 AS create_var2 + CALL { + WITH create_var2 + CREATE (create_this1:\`Movie\`) + SET + create_this1.id = create_var2.id + WITH create_this1, create_var2 + CALL { + WITH create_this1, create_var2 + UNWIND create_var2.actors.create AS create_var3 + WITH create_var3.node AS create_var4, create_var3.edge AS create_var5, create_this1 + CREATE (create_this6:\`Actor\`) + SET + create_this6.name = create_var4.name, + create_this6.id = randomUUID() + MERGE (create_this6)-[create_this7:ACTED_IN]->(create_this1) + SET + create_this7.year = create_var5.year + WITH create_this6 + CALL { + WITH create_this6 + MATCH (create_this6)-[create_this6_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this6_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS create_this6_website_Website_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this1 + CALL { + WITH create_this1 + MATCH (create_this1)-[create_this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(create_this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS create_this1_website_Website_unique_ignored + } + RETURN create_this1 + } + CALL { + WITH create_this1 + MATCH (create_this1_actors:\`Actor\`)-[create_this0:ACTED_IN]->(create_this1) + WITH create_this1_actors { .name } AS create_this1_actors + RETURN collect(create_this1_actors) AS create_this1_actors + } + RETURN collect(create_this1 { .id, actors: create_this1_actors }) AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + }, + \\"edge\\": { + \\"year\\": { + \\"low\\": 2022, + \\"high\\": 0 + } + } + } + ] + } + } + ], + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("connect", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { connect: { where: { node: { id: "3" } } } } } + { id: "2", actors: { connect: { where: { node: { id: "4" } } } } } + ] + ) { + movies { + id + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", {}); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_actors_connect0_node:Actor) + WHERE this0_actors_connect0_node.id = $this0_actors_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_actors_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_actors_connect0_node + MERGE (this0)<-[this0_actors_connect0_relationship:ACTED_IN]-(this0_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this0_actors_connect_Actor + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CALL { + WITH this1 + OPTIONAL MATCH (this1_actors_connect0_node:Actor) + WHERE this1_actors_connect0_node.id = $this1_actors_connect0_node_param0 + CALL { + WITH * + WITH collect(this1_actors_connect0_node) as connectedNodes, collect(this1) as parentNodes + UNWIND parentNodes as this1 + UNWIND connectedNodes as this1_actors_connect0_node + MERGE (this1)<-[this1_actors_connect0_relationship:ACTED_IN]-(this1_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this1_actors_connect_Actor + } + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + WITH this0 + MATCH (this0_actors:\`Actor\`)-[create_this0:ACTED_IN]->(this0) + WITH this0_actors { .name } AS this0_actors + RETURN collect(this0_actors) AS this0_actors + } + CALL { + WITH this1 + MATCH (this1_actors:\`Actor\`)-[create_this0:ACTED_IN]->(this1) + WITH this1_actors { .name } AS this1_actors + RETURN collect(this1_actors) AS this1_actors + } + RETURN [ + this0 { .id, actors: this0_actors }, + this1 { .id, actors: this1_actors }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this0_actors_connect0_node_param0\\": \\"3\\", + \\"this1_id\\": \\"2\\", + \\"this1_actors_connect0_node_param0\\": \\"4\\", + \\"resolvedCallbacks\\": {} + }" + `); + }); + + test("heterogeneous batch", async () => { + const query = gql` + mutation { + createMovies( + input: [ + { id: "1", actors: { create: [{ node: { name: "actor 1" }, edge: { year: 2022 } }] } } + { id: "2", actors: { create: [{ node: { name: "actor 2" }, edge: { year: 1999 } }] } } + { id: "3", website: { create: { node: { address: "mywebsite.com" } } } } + { id: "4", actors: { connect: { where: { node: { id: "2" } } } } } + { + id: "5" + actors: { + connectOrCreate: { + where: { node: { id: "2" } } + onCreate: { node: { name: "actor 2" } } + } + } + } + ] + ) { + movies { + id + website { + address + } + actors { + name + } + } + } + } + `; + + const req = createJwtRequest("secret", {}); + const result = await translateQuery(neoSchema, query, { + req, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CREATE (this0_actors0_node:Actor) + SET this0_actors0_node.id = randomUUID() + SET this0_actors0_node.name = $this0_actors0_node_name + MERGE (this0)<-[this0_actors0_relationship:ACTED_IN]-(this0_actors0_node) + SET this0_actors0_relationship.year = $this0_actors0_relationship_year + WITH this0, this0_actors0_node + CALL { + WITH this0_actors0_node + MATCH (this0_actors0_node)-[this0_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this0_actors0_node_website_Website_unique_ignored + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this0_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this0_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this0_website_Website_unique_ignored + } + RETURN this0 + } + CALL { + CREATE (this1:Movie) + SET this1.id = $this1_id + WITH this1 + CREATE (this1_actors0_node:Actor) + SET this1_actors0_node.id = randomUUID() + SET this1_actors0_node.name = $this1_actors0_node_name + MERGE (this1)<-[this1_actors0_relationship:ACTED_IN]-(this1_actors0_node) + SET this1_actors0_relationship.year = $this1_actors0_relationship_year + WITH this1, this1_actors0_node + CALL { + WITH this1_actors0_node + MATCH (this1_actors0_node)-[this1_actors0_node_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_actors0_node_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDActor.website must be less than or equal to one', [0]) + RETURN c AS this1_actors0_node_website_Website_unique_ignored + } + WITH this1 + CALL { + WITH this1 + MATCH (this1)-[this1_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this1_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this1_website_Website_unique_ignored + } + RETURN this1 + } + CALL { + CREATE (this2:Movie) + SET this2.id = $this2_id + WITH this2 + CREATE (this2_website0_node:Website) + SET this2_website0_node.address = $this2_website0_node_address + MERGE (this2)-[:HAS_WEBSITE]->(this2_website0_node) + WITH this2 + CALL { + WITH this2 + MATCH (this2)-[this2_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this2_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this2_website_Website_unique_ignored + } + RETURN this2 + } + CALL { + CREATE (this3:Movie) + SET this3.id = $this3_id + WITH this3 + CALL { + WITH this3 + OPTIONAL MATCH (this3_actors_connect0_node:Actor) + WHERE this3_actors_connect0_node.id = $this3_actors_connect0_node_param0 + CALL { + WITH * + WITH collect(this3_actors_connect0_node) as connectedNodes, collect(this3) as parentNodes + UNWIND parentNodes as this3 + UNWIND connectedNodes as this3_actors_connect0_node + MERGE (this3)<-[this3_actors_connect0_relationship:ACTED_IN]-(this3_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this3_actors_connect_Actor + } + WITH this3 + CALL { + WITH this3 + MATCH (this3)-[this3_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this3_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this3_website_Website_unique_ignored + } + RETURN this3 + } + CALL { + CREATE (this4:Movie) + SET this4.id = $this4_id + WITH this4 + CALL { + WITH this4 + MERGE (this4_actors_connectOrCreate0:\`Actor\` { id: $this4_actors_connectOrCreate_param0 }) + ON CREATE SET + this4_actors_connectOrCreate0.name = $this4_actors_connectOrCreate_param1 + MERGE (this4_actors_connectOrCreate0)-[this4_actors_connectOrCreate_this0:ACTED_IN]->(this4) + RETURN COUNT(*) AS _ + } + WITH this4 + CALL { + WITH this4 + MATCH (this4)-[this4_website_Website_unique:HAS_WEBSITE]->(:Website) + WITH count(this4_website_Website_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.website must be less than or equal to one', [0]) + RETURN c AS this4_website_Website_unique_ignored + } + RETURN this4 + } + CALL { + WITH this0 + MATCH (this0)-[create_this0:HAS_WEBSITE]->(this0_website:\`Website\`) + WITH this0_website { .address } AS this0_website + RETURN head(collect(this0_website)) AS this0_website + } + CALL { + WITH this0 + MATCH (this0_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this0) + WITH this0_actors { .name } AS this0_actors + RETURN collect(this0_actors) AS this0_actors + } + CALL { + WITH this1 + MATCH (this1)-[create_this0:HAS_WEBSITE]->(this1_website:\`Website\`) + WITH this1_website { .address } AS this1_website + RETURN head(collect(this1_website)) AS this1_website + } + CALL { + WITH this1 + MATCH (this1_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this1) + WITH this1_actors { .name } AS this1_actors + RETURN collect(this1_actors) AS this1_actors + } + CALL { + WITH this2 + MATCH (this2)-[create_this0:HAS_WEBSITE]->(this2_website:\`Website\`) + WITH this2_website { .address } AS this2_website + RETURN head(collect(this2_website)) AS this2_website + } + CALL { + WITH this2 + MATCH (this2_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this2) + WITH this2_actors { .name } AS this2_actors + RETURN collect(this2_actors) AS this2_actors + } + CALL { + WITH this3 + MATCH (this3)-[create_this0:HAS_WEBSITE]->(this3_website:\`Website\`) + WITH this3_website { .address } AS this3_website + RETURN head(collect(this3_website)) AS this3_website + } + CALL { + WITH this3 + MATCH (this3_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this3) + WITH this3_actors { .name } AS this3_actors + RETURN collect(this3_actors) AS this3_actors + } + CALL { + WITH this4 + MATCH (this4)-[create_this0:HAS_WEBSITE]->(this4_website:\`Website\`) + WITH this4_website { .address } AS this4_website + RETURN head(collect(this4_website)) AS this4_website + } + CALL { + WITH this4 + MATCH (this4_actors:\`Actor\`)-[create_this1:ACTED_IN]->(this4) + WITH this4_actors { .name } AS this4_actors + RETURN collect(this4_actors) AS this4_actors + } + RETURN [ + this0 { .id, website: this0_website, actors: this0_actors }, + this1 { .id, website: this1_website, actors: this1_actors }, + this2 { .id, website: this2_website, actors: this2_actors }, + this3 { .id, website: this3_website, actors: this3_actors }, + this4 { .id, website: this4_website, actors: this4_actors }] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"this0_id\\": \\"1\\", + \\"this0_actors0_node_name\\": \\"actor 1\\", + \\"this0_actors0_relationship_year\\": { + \\"low\\": 2022, + \\"high\\": 0 + }, + \\"this1_id\\": \\"2\\", + \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"this1_actors0_relationship_year\\": { + \\"low\\": 1999, + \\"high\\": 0 + }, + \\"this2_id\\": \\"3\\", + \\"this2_website0_node_address\\": \\"mywebsite.com\\", + \\"this3_id\\": \\"4\\", + \\"this3_actors_connect0_node_param0\\": \\"2\\", + \\"this4_id\\": \\"5\\", + \\"this4_actors_connectOrCreate_param0\\": \\"2\\", + \\"this4_actors_connectOrCreate_param1\\": \\"actor 2\\", + \\"resolvedCallbacks\\": {} + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/operations/create.test.ts b/packages/graphql/tests/tck/operations/create.test.ts index 127ab141c0..b4f99760e2 100644 --- a/packages/graphql/tests/tck/operations/create.test.ts +++ b/packages/graphql/tests/tck/operations/create.test.ts @@ -63,18 +63,24 @@ describe("Cypher Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + RETURN create_this0 } - RETURN [ - this0 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -97,25 +103,27 @@ describe("Cypher Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:Movie) - SET this1.id = $this1_id - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + RETURN create_this0 } - RETURN [ - this0 { .id }, - this1 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this1_id\\": \\"2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\" + }, + { + \\"id\\": \\"2\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -143,35 +151,56 @@ describe("Cypher Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_actors0_node:Actor) - SET this0_actors0_node.name = $this0_actors0_node_name - MERGE (this0)<-[:ACTED_IN]-(this0_actors0_node) - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:Movie) - SET this1.id = $this1_id - WITH this1 - CREATE (this1_actors0_node:Actor) - SET this1_actors0_node.name = $this1_actors0_node_name - MERGE (this1)<-[:ACTED_IN]-(this1_actors0_node) - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`) + SET + create_this5.name = create_var3.name + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + RETURN collect(NULL) + } + RETURN create_this0 } - RETURN [ - this0 { .id }, - this1 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this0_actors0_node_name\\": \\"actor 1\\", - \\"this1_id\\": \\"2\\", - \\"this1_actors0_node_name\\": \\"actor 2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\" + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 2\\" + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -209,45 +238,85 @@ describe("Cypher Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_actors0_node:Actor) - SET this0_actors0_node.name = $this0_actors0_node_name - WITH this0, this0_actors0_node - CREATE (this0_actors0_node_movies0_node:Movie) - SET this0_actors0_node_movies0_node.id = $this0_actors0_node_movies0_node_id - MERGE (this0_actors0_node)-[:ACTED_IN]->(this0_actors0_node_movies0_node) - MERGE (this0)<-[:ACTED_IN]-(this0_actors0_node) - RETURN this0 - } + "UNWIND $create_param0 AS create_var1 CALL { - CREATE (this1:Movie) - SET this1.id = $this1_id - WITH this1 - CREATE (this1_actors0_node:Actor) - SET this1_actors0_node.name = $this1_actors0_node_name - WITH this1, this1_actors0_node - CREATE (this1_actors0_node_movies0_node:Movie) - SET this1_actors0_node_movies0_node.id = $this1_actors0_node_movies0_node_id - MERGE (this1_actors0_node)-[:ACTED_IN]->(this1_actors0_node_movies0_node) - MERGE (this1)<-[:ACTED_IN]-(this1_actors0_node) - RETURN this1 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.actors.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Actor\`) + SET + create_this5.name = create_var3.name + MERGE (create_this5)-[create_this6:ACTED_IN]->(create_this0) + WITH create_this5, create_var3 + CALL { + WITH create_this5, create_var3 + UNWIND create_var3.movies.create AS create_var7 + WITH create_var7.node AS create_var8, create_var7.edge AS create_var9, create_this5 + CREATE (create_this10:\`Movie\`) + SET + create_this10.id = create_var8.id + MERGE (create_this5)-[create_this11:ACTED_IN]->(create_this10) + RETURN collect(NULL) + } + RETURN collect(NULL) + } + RETURN create_this0 } - RETURN [ - this0 { .id }, - this1 { .id }] AS data" + RETURN collect(create_this0 { .id }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"1\\", - \\"this0_actors0_node_name\\": \\"actor 1\\", - \\"this0_actors0_node_movies0_node_id\\": \\"10\\", - \\"this1_id\\": \\"2\\", - \\"this1_actors0_node_name\\": \\"actor 2\\", - \\"this1_actors0_node_movies0_node_id\\": \\"20\\", + \\"create_param0\\": [ + { + \\"id\\": \\"1\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 1\\", + \\"movies\\": { + \\"create\\": [ + { + \\"node\\": { + \\"id\\": \\"10\\" + } + } + ] + } + } + } + ] + } + }, + { + \\"id\\": \\"2\\", + \\"actors\\": { + \\"create\\": [ + { + \\"node\\": { + \\"name\\": \\"actor 2\\", + \\"movies\\": { + \\"create\\": [ + { + \\"node\\": { + \\"id\\": \\"20\\" + } + } + ] + } + } + } + ] + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -270,29 +339,29 @@ describe("Cypher Create", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"CALL { -CREATE (this0:Movie) -SET this0.id = $this0_id -WITH this0 -CALL { - WITH this0 - OPTIONAL MATCH (this0_actors_connect0_node:Actor) - WHERE this0_actors_connect0_node.name = $this0_actors_connect0_node_param0 - CALL { - WITH * - WITH collect(this0_actors_connect0_node) as connectedNodes, collect(this0) as parentNodes - UNWIND parentNodes as this0 - UNWIND connectedNodes as this0_actors_connect0_node - MERGE (this0)<-[:ACTED_IN]-(this0_actors_connect0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this0_actors_connect_Actor -} -RETURN this0 -} -RETURN [ -this0 { .id }] AS data" -`); + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_actors_connect0_node:Actor) + WHERE this0_actors_connect0_node.name = $this0_actors_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_actors_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_actors_connect0_node + MERGE (this0)<-[:ACTED_IN]-(this0_actors_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this0_actors_connect_Actor + } + RETURN this0 + } + RETURN [ + this0 { .id }] AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -330,44 +399,44 @@ this0 { .id }] AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"CALL { -CREATE (this0:Actor) -SET this0.name = $this0_name -WITH this0 -CALL { - WITH this0 - OPTIONAL MATCH (this0_movies_connect0_node:Movie) - WHERE this0_movies_connect0_node.id = $this0_movies_connect0_node_param0 - CALL { - WITH * - WITH collect(this0_movies_connect0_node) as connectedNodes, collect(this0) as parentNodes - UNWIND parentNodes as this0 - UNWIND connectedNodes as this0_movies_connect0_node - MERGE (this0)-[:ACTED_IN]->(this0_movies_connect0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this0_movies_connect_Movie -} -RETURN this0 -} -CALL { - WITH this0 - MATCH (this0)-[create_this0:ACTED_IN]->(this0_movies:\`Movie\`) - CALL { - WITH this0_movies - MATCH (this0_movies)<-[this0_movies_connection_actorsConnectionthis0:ACTED_IN]-(this0_movies_Actor:\`Actor\`) - WHERE this0_movies_Actor.name = $projection_movies_connection_actorsConnectionparam0 - WITH { node: { name: this0_movies_Actor.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS this0_movies_actorsConnection - } - WITH this0_movies { actorsConnection: this0_movies_actorsConnection } AS this0_movies - RETURN collect(this0_movies) AS this0_movies -} -RETURN [ -this0 { .name, movies: this0_movies }] AS data" -`); + "CALL { + CREATE (this0:Actor) + SET this0.name = $this0_name + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_movies_connect0_node:Movie) + WHERE this0_movies_connect0_node.id = $this0_movies_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_movies_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_movies_connect0_node + MERGE (this0)-[:ACTED_IN]->(this0_movies_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this0_movies_connect_Movie + } + RETURN this0 + } + CALL { + WITH this0 + MATCH (this0)-[create_this0:ACTED_IN]->(this0_movies:\`Movie\`) + CALL { + WITH this0_movies + MATCH (this0_movies)<-[this0_movies_connection_actorsConnectionthis0:ACTED_IN]-(this0_movies_Actor:\`Actor\`) + WHERE this0_movies_Actor.name = $projection_movies_connection_actorsConnectionparam0 + WITH { node: { name: this0_movies_Actor.name } } AS edge + WITH collect(edge) AS edges + WITH edges, size(edges) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS this0_movies_actorsConnection + } + WITH this0_movies { actorsConnection: this0_movies_actorsConnection } AS this0_movies + RETURN collect(this0_movies) AS this0_movies + } + RETURN [ + this0 { .name, movies: this0_movies }] AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/projection.test.ts b/packages/graphql/tests/tck/projection.test.ts index fd70403b7a..e17800a035 100644 --- a/packages/graphql/tests/tck/projection.test.ts +++ b/packages/graphql/tests/tck/projection.test.ts @@ -100,67 +100,39 @@ describe("Cypher Projection", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Product) - SET this0.id = $this0_id - RETURN this0 - } + "UNWIND $create_param3 AS create_var4 CALL { - CREATE (this1:Product) - SET this1.id = $this1_id - RETURN this1 + WITH create_var4 + CREATE (create_this3:\`Product\`) + SET + create_this3.id = create_var4.id + RETURN create_this3 } CALL { - WITH this0 - MATCH (this0)-[create_this0:HAS_PHOTO]->(this0_photos:\`Photo\`) - WHERE this0_photos.url = $create_param0 - WITH this0_photos { .url, location: (CASE - WHEN this0_photos.location IS NOT NULL THEN { point: this0_photos.location } + WITH create_this3 + MATCH (create_this3)-[create_this0:HAS_PHOTO]->(create_this3_photos:\`Photo\`) + WHERE create_this3_photos.url = $create_param0 + WITH create_this3_photos { .url, location: (CASE + WHEN create_this3_photos.location IS NOT NULL THEN { point: create_this3_photos.location } ELSE NULL - END) } AS this0_photos - RETURN collect(this0_photos) AS this0_photos + END) } AS create_this3_photos + RETURN collect(create_this3_photos) AS create_this3_photos } CALL { - WITH this0 - MATCH (this0)-[create_this1:HAS_COLOR]->(this0_colors:\`Color\`) - WHERE this0_colors.id = $create_param1 - WITH this0_colors { .id } AS this0_colors - RETURN collect(this0_colors) AS this0_colors + WITH create_this3 + MATCH (create_this3)-[create_this1:HAS_COLOR]->(create_this3_colors:\`Color\`) + WHERE create_this3_colors.id = $create_param1 + WITH create_this3_colors { .id } AS create_this3_colors + RETURN collect(create_this3_colors) AS create_this3_colors } CALL { - WITH this0 - MATCH (this0)-[create_this2:HAS_SIZE]->(this0_sizes:\`Size\`) - WHERE this0_sizes.name = $create_param2 - WITH this0_sizes { .name } AS this0_sizes - RETURN collect(this0_sizes) AS this0_sizes + WITH create_this3 + MATCH (create_this3)-[create_this2:HAS_SIZE]->(create_this3_sizes:\`Size\`) + WHERE create_this3_sizes.name = $create_param2 + WITH create_this3_sizes { .name } AS create_this3_sizes + RETURN collect(create_this3_sizes) AS create_this3_sizes } - CALL { - WITH this1 - MATCH (this1)-[create_this0:HAS_PHOTO]->(this1_photos:\`Photo\`) - WHERE this1_photos.url = $create_param0 - WITH this1_photos { .url, location: (CASE - WHEN this1_photos.location IS NOT NULL THEN { point: this1_photos.location } - ELSE NULL - END) } AS this1_photos - RETURN collect(this1_photos) AS this1_photos - } - CALL { - WITH this1 - MATCH (this1)-[create_this1:HAS_COLOR]->(this1_colors:\`Color\`) - WHERE this1_colors.id = $create_param1 - WITH this1_colors { .id } AS this1_colors - RETURN collect(this1_colors) AS this1_colors - } - CALL { - WITH this1 - MATCH (this1)-[create_this2:HAS_SIZE]->(this1_sizes:\`Size\`) - WHERE this1_sizes.name = $create_param2 - WITH this1_sizes { .name } AS this1_sizes - RETURN collect(this1_sizes) AS this1_sizes - } - RETURN [ - this0 { .id, photos: this0_photos, colors: this0_colors, sizes: this0_sizes }, - this1 { .id, photos: this1_photos, colors: this1_colors, sizes: this1_sizes }] AS data" + RETURN collect(create_this3 { .id, photos: create_this3_photos, colors: create_this3_colors, sizes: create_this3_sizes }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -168,8 +140,14 @@ describe("Cypher Projection", () => { \\"create_param0\\": \\"url.com\\", \\"create_param1\\": \\"123\\", \\"create_param2\\": \\"small\\", - \\"this0_id\\": \\"1\\", - \\"this1_id\\": \\"2\\", + \\"create_param3\\": [ + { + \\"id\\": \\"1\\" + }, + { + \\"id\\": \\"2\\" + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/rfcs/rfc-003.test.ts b/packages/graphql/tests/tck/rfcs/rfc-003.test.ts index fef2367ce7..6a97dd3c7b 100644 --- a/packages/graphql/tests/tck/rfcs/rfc-003.test.ts +++ b/packages/graphql/tests/tck/rfcs/rfc-003.test.ts @@ -56,25 +56,32 @@ describe("tck/rfs/003", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 + "UNWIND $create_param0 AS create_var1 CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this0_director_Director_unique_ignored - } - RETURN this0 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)<-[create_this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(create_this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS create_this0_director_Director_unique_ignored + } + RETURN create_this0 } RETURN 'Query cannot conclude with CALL'" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"movieId-1\\", + \\"create_param0\\": [ + { + \\"id\\": \\"movieId-1\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -111,25 +118,32 @@ describe("tck/rfs/003", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 + "UNWIND $create_param0 AS create_var1 CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) - RETURN c AS this0_director_Director_unique_ignored - } - RETURN this0 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)<-[create_this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(create_this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) + RETURN c AS create_this0_director_Director_unique_ignored + } + RETURN create_this0 } RETURN 'Query cannot conclude with CALL'" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"movieId-1\\", + \\"create_param0\\": [ + { + \\"id\\": \\"movieId-1\\" + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -173,38 +187,58 @@ describe("tck/rfs/003", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_director0_node:Director) - SET this0_director0_node.id = $this0_director0_node_id - MERGE (this0)<-[:DIRECTED]-(this0_director0_node) - WITH this0, this0_director0_node + "UNWIND $create_param0 AS create_var1 CALL { - WITH this0_director0_node - MATCH (this0_director0_node)-[this0_director0_node_address_Address_unique:HAS_ADDRESS]->(:Address) - WITH count(this0_director0_node_address_Address_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address required', [0]) - RETURN c AS this0_director0_node_address_Address_unique_ignored - } - WITH this0 - CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this0_director_Director_unique_ignored - } - RETURN this0 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.director.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Director\`) + SET + create_this5.id = create_var3.id + MERGE (create_this5)-[create_this6:DIRECTED]->(create_this0) + WITH create_this5 + CALL { + WITH create_this5 + MATCH (create_this5)-[create_this5_address_Address_unique:HAS_ADDRESS]->(:Address) + WITH count(create_this5_address_Address_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address required', [0]) + RETURN c AS create_this5_address_Address_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)<-[create_this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(create_this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS create_this0_director_Director_unique_ignored + } + RETURN create_this0 } RETURN 'Query cannot conclude with CALL'" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"movieId-2\\", - \\"this0_director0_node_id\\": \\"directorId-2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"movieId-2\\", + \\"director\\": { + \\"create\\": { + \\"node\\": { + \\"id\\": \\"directorId-2\\" + } + } + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -247,38 +281,58 @@ describe("tck/rfs/003", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.id = $this0_id - WITH this0 - CREATE (this0_director0_node:Director) - SET this0_director0_node.id = $this0_director0_node_id - MERGE (this0)<-[:DIRECTED]-(this0_director0_node) - WITH this0, this0_director0_node - CALL { - WITH this0_director0_node - MATCH (this0_director0_node)-[this0_director0_node_address_Address_unique:HAS_ADDRESS]->(:Address) - WITH count(this0_director0_node_address_Address_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address must be less than or equal to one', [0]) - RETURN c AS this0_director0_node_address_Address_unique_ignored - } - WITH this0 + "UNWIND $create_param0 AS create_var1 CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) - RETURN c AS this0_director_Director_unique_ignored - } - RETURN this0 + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.id = create_var1.id + WITH create_this0, create_var1 + CALL { + WITH create_this0, create_var1 + UNWIND create_var1.director.create AS create_var2 + WITH create_var2.node AS create_var3, create_var2.edge AS create_var4, create_this0 + CREATE (create_this5:\`Director\`) + SET + create_this5.id = create_var3.id + MERGE (create_this5)-[create_this6:DIRECTED]->(create_this0) + WITH create_this5 + CALL { + WITH create_this5 + MATCH (create_this5)-[create_this5_address_Address_unique:HAS_ADDRESS]->(:Address) + WITH count(create_this5_address_Address_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address must be less than or equal to one', [0]) + RETURN c AS create_this5_address_Address_unique_ignored + } + RETURN collect(NULL) + } + WITH create_this0 + CALL { + WITH create_this0 + MATCH (create_this0)<-[create_this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(create_this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) + RETURN c AS create_this0_director_Director_unique_ignored + } + RETURN create_this0 } RETURN 'Query cannot conclude with CALL'" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_id\\": \\"movieId-2\\", - \\"this0_director0_node_id\\": \\"directorId-2\\", + \\"create_param0\\": [ + { + \\"id\\": \\"movieId-2\\", + \\"director\\": { + \\"create\\": { + \\"node\\": { + \\"id\\": \\"directorId-2\\" + } + } + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -711,46 +765,46 @@ describe("tck/rfs/003", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Movie\`) -WHERE this.id = $param0 -WITH this -OPTIONAL MATCH (this)<-[this_delete_director0_relationship:DIRECTED]-(this_delete_director0:Director) -WHERE this_delete_director0.id = $updateMovies_args_delete_director_where_Directorparam0 -WITH this, this_delete_director0 -OPTIONAL MATCH (this_delete_director0)-[this_delete_director0_address0_relationship:HAS_ADDRESS]->(this_delete_director0_address0:Address) -WHERE this_delete_director0_address0.id = $updateMovies_args_delete_director_delete_address_where_Addressparam0 -WITH this, this_delete_director0, collect(DISTINCT this_delete_director0_address0) as this_delete_director0_address0_to_delete -CALL { - WITH this_delete_director0_address0_to_delete - UNWIND this_delete_director0_address0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this, collect(DISTINCT this_delete_director0) as this_delete_director0_to_delete -CALL { - WITH this_delete_director0_to_delete - UNWIND this_delete_director0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH * -WITH * -CALL { - WITH this - MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) - WITH count(this_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this_director_Director_unique_ignored -} -CALL { - WITH this - MATCH (this)<-[this_coDirector_CoDirector_unique:CO_DIRECTED]-(:CoDirector) - WITH count(this_coDirector_CoDirector_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.coDirector must be less than or equal to one', [0]) - RETURN c AS this_coDirector_CoDirector_unique_ignored -} -RETURN 'Query cannot conclude with CALL'" -`); + "MATCH (this:\`Movie\`) + WHERE this.id = $param0 + WITH this + OPTIONAL MATCH (this)<-[this_delete_director0_relationship:DIRECTED]-(this_delete_director0:Director) + WHERE this_delete_director0.id = $updateMovies_args_delete_director_where_Directorparam0 + WITH this, this_delete_director0 + OPTIONAL MATCH (this_delete_director0)-[this_delete_director0_address0_relationship:HAS_ADDRESS]->(this_delete_director0_address0:Address) + WHERE this_delete_director0_address0.id = $updateMovies_args_delete_director_delete_address_where_Addressparam0 + WITH this, this_delete_director0, collect(DISTINCT this_delete_director0_address0) as this_delete_director0_address0_to_delete + CALL { + WITH this_delete_director0_address0_to_delete + UNWIND this_delete_director0_address0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this, collect(DISTINCT this_delete_director0) as this_delete_director0_to_delete + CALL { + WITH this_delete_director0_to_delete + UNWIND this_delete_director0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH * + WITH * + CALL { + WITH this + MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) + WITH count(this_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS this_director_Director_unique_ignored + } + CALL { + WITH this + MATCH (this)<-[this_coDirector_CoDirector_unique:CO_DIRECTED]-(:CoDirector) + WITH count(this_coDirector_CoDirector_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.coDirector must be less than or equal to one', [0]) + RETURN c AS this_coDirector_CoDirector_unique_ignored + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -834,46 +888,46 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Movie\`) -WHERE this.id = $param0 -WITH this -OPTIONAL MATCH (this)<-[this_delete_director0_relationship:DIRECTED]-(this_delete_director0:Director) -WHERE this_delete_director0.id = $updateMovies_args_delete_director_where_Directorparam0 -WITH this, this_delete_director0 -OPTIONAL MATCH (this_delete_director0)-[this_delete_director0_address0_relationship:HAS_ADDRESS]->(this_delete_director0_address0:Address) -WHERE this_delete_director0_address0.id = $updateMovies_args_delete_director_delete_address_where_Addressparam0 -WITH this, this_delete_director0, collect(DISTINCT this_delete_director0_address0) as this_delete_director0_address0_to_delete -CALL { - WITH this_delete_director0_address0_to_delete - UNWIND this_delete_director0_address0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH this, collect(DISTINCT this_delete_director0) as this_delete_director0_to_delete -CALL { - WITH this_delete_director0_to_delete - UNWIND this_delete_director0_to_delete AS x - DETACH DELETE x - RETURN count(*) AS _ -} -WITH * -WITH * -CALL { - WITH this - MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) - WITH count(this_director_Director_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) - RETURN c AS this_director_Director_unique_ignored -} -CALL { - WITH this - MATCH (this)<-[this_coDirector_CoDirector_unique:CO_DIRECTED]-(:CoDirector) - WITH count(this_coDirector_CoDirector_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.coDirector must be less than or equal to one', [0]) - RETURN c AS this_coDirector_CoDirector_unique_ignored -} -RETURN 'Query cannot conclude with CALL'" -`); + "MATCH (this:\`Movie\`) + WHERE this.id = $param0 + WITH this + OPTIONAL MATCH (this)<-[this_delete_director0_relationship:DIRECTED]-(this_delete_director0:Director) + WHERE this_delete_director0.id = $updateMovies_args_delete_director_where_Directorparam0 + WITH this, this_delete_director0 + OPTIONAL MATCH (this_delete_director0)-[this_delete_director0_address0_relationship:HAS_ADDRESS]->(this_delete_director0_address0:Address) + WHERE this_delete_director0_address0.id = $updateMovies_args_delete_director_delete_address_where_Addressparam0 + WITH this, this_delete_director0, collect(DISTINCT this_delete_director0_address0) as this_delete_director0_address0_to_delete + CALL { + WITH this_delete_director0_address0_to_delete + UNWIND this_delete_director0_address0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH this, collect(DISTINCT this_delete_director0) as this_delete_director0_to_delete + CALL { + WITH this_delete_director0_to_delete + UNWIND this_delete_director0_to_delete AS x + DETACH DELETE x + RETURN count(*) AS _ + } + WITH * + WITH * + CALL { + WITH this + MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) + WITH count(this_director_Director_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) + RETURN c AS this_director_Director_unique_ignored + } + CALL { + WITH this + MATCH (this)<-[this_coDirector_CoDirector_unique:CO_DIRECTED]-(:CoDirector) + WITH count(this_coDirector_CoDirector_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.coDirector must be less than or equal to one', [0]) + RETURN c AS this_coDirector_CoDirector_unique_ignored + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -942,36 +996,36 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"CALL { -CREATE (this0:Movie) -SET this0.id = $this0_id -WITH this0 -CALL { - WITH this0 - OPTIONAL MATCH (this0_director_connect0_node:Director) - WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 - CALL { - WITH * - WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes - UNWIND parentNodes as this0 - UNWIND connectedNodes as this0_director_connect0_node - MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this0_director_connect_Director -} -WITH this0 -CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this0_director_Director_unique_ignored -} -RETURN this0 -} -RETURN 'Query cannot conclude with CALL'" -`); + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_director_connect0_node:Director) + WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_director_connect0_node + MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this0_director_connect_Director + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS this0_director_Director_unique_ignored + } + RETURN this0 + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -1014,36 +1068,36 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"CALL { -CREATE (this0:Movie) -SET this0.id = $this0_id -WITH this0 -CALL { - WITH this0 - OPTIONAL MATCH (this0_director_connect0_node:Director) - WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 - CALL { - WITH * - WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes - UNWIND parentNodes as this0 - UNWIND connectedNodes as this0_director_connect0_node - MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this0_director_connect_Director -} -WITH this0 -CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) - RETURN c AS this0_director_Director_unique_ignored -} -RETURN this0 -} -RETURN 'Query cannot conclude with CALL'" -`); + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_director_connect0_node:Director) + WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_director_connect0_node + MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this0_director_connect_Director + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) + RETURN c AS this0_director_Director_unique_ignored + } + RETURN this0 + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -1104,59 +1158,59 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"CALL { -CREATE (this0:Movie) -SET this0.id = $this0_id -WITH this0 -CALL { - WITH this0 - OPTIONAL MATCH (this0_director_connect0_node:Director) - WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 - CALL { - WITH * - WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes - UNWIND parentNodes as this0 - UNWIND connectedNodes as this0_director_connect0_node - MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) - RETURN count(*) AS _ - } -WITH this0, this0_director_connect0_node -CALL { - WITH this0, this0_director_connect0_node - OPTIONAL MATCH (this0_director_connect0_node_address0_node:Address) - WHERE this0_director_connect0_node_address0_node.street = $this0_director_connect0_node_address0_node_param0 - CALL { - WITH * - WITH this0, collect(this0_director_connect0_node_address0_node) as connectedNodes, collect(this0_director_connect0_node) as parentNodes - UNWIND parentNodes as this0_director_connect0_node - UNWIND connectedNodes as this0_director_connect0_node_address0_node - MERGE (this0_director_connect0_node)-[:HAS_ADDRESS]->(this0_director_connect0_node_address0_node) - RETURN count(*) AS _ - } - WITH this0, this0_director_connect0_node, this0_director_connect0_node_address0_node -CALL { - WITH this0_director_connect0_node - MATCH (this0_director_connect0_node)-[this0_director_connect0_node_address_Address_unique:HAS_ADDRESS]->(:Address) - WITH count(this0_director_connect0_node_address_Address_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address required', [0]) - RETURN c AS this0_director_connect0_node_address_Address_unique_ignored -} - RETURN count(*) AS connect_this0_director_connect0_node_address_Address -} - RETURN count(*) AS connect_this0_director_connect_Director -} -WITH this0 -CALL { - WITH this0 - MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) - WITH count(this0_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this0_director_Director_unique_ignored -} -RETURN this0 -} -RETURN 'Query cannot conclude with CALL'" -`); + "CALL { + CREATE (this0:Movie) + SET this0.id = $this0_id + WITH this0 + CALL { + WITH this0 + OPTIONAL MATCH (this0_director_connect0_node:Director) + WHERE this0_director_connect0_node.id = $this0_director_connect0_node_param0 + CALL { + WITH * + WITH collect(this0_director_connect0_node) as connectedNodes, collect(this0) as parentNodes + UNWIND parentNodes as this0 + UNWIND connectedNodes as this0_director_connect0_node + MERGE (this0)<-[:DIRECTED]-(this0_director_connect0_node) + RETURN count(*) AS _ + } + WITH this0, this0_director_connect0_node + CALL { + WITH this0, this0_director_connect0_node + OPTIONAL MATCH (this0_director_connect0_node_address0_node:Address) + WHERE this0_director_connect0_node_address0_node.street = $this0_director_connect0_node_address0_node_param0 + CALL { + WITH * + WITH this0, collect(this0_director_connect0_node_address0_node) as connectedNodes, collect(this0_director_connect0_node) as parentNodes + UNWIND parentNodes as this0_director_connect0_node + UNWIND connectedNodes as this0_director_connect0_node_address0_node + MERGE (this0_director_connect0_node)-[:HAS_ADDRESS]->(this0_director_connect0_node_address0_node) + RETURN count(*) AS _ + } + WITH this0, this0_director_connect0_node, this0_director_connect0_node_address0_node + CALL { + WITH this0_director_connect0_node + MATCH (this0_director_connect0_node)-[this0_director_connect0_node_address_Address_unique:HAS_ADDRESS]->(:Address) + WITH count(this0_director_connect0_node_address_Address_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDDirector.address required', [0]) + RETURN c AS this0_director_connect0_node_address_Address_unique_ignored + } + RETURN count(*) AS connect_this0_director_connect0_node_address_Address + } + RETURN count(*) AS connect_this0_director_connect_Director + } + WITH this0 + CALL { + WITH this0 + MATCH (this0)<-[this0_director_Director_unique:DIRECTED]-(:Director) + WITH count(this0_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS this0_director_Director_unique_ignored + } + RETURN this0 + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -1203,33 +1257,33 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Movie\`) -WHERE this.id = $param0 -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) -WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 -CALL { - WITH this_disconnect_director0, this_disconnect_director0_rel - WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel - UNWIND this_disconnect_director0 as x - DELETE this_disconnect_director0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_director_Director -} -WITH * -WITH * -CALL { - WITH this - MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) - WITH count(this_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this_director_Director_unique_ignored -} -RETURN 'Query cannot conclude with CALL'" -`); + "MATCH (this:\`Movie\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) + WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 + CALL { + WITH this_disconnect_director0, this_disconnect_director0_rel + WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel + UNWIND this_disconnect_director0 as x + DELETE this_disconnect_director0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_director_Director + } + WITH * + WITH * + CALL { + WITH this + MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) + WITH count(this_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS this_director_Director_unique_ignored + } + RETURN 'Query cannot conclude with CALL'" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -1299,54 +1353,54 @@ RETURN 'Query cannot conclude with CALL'" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Movie\`) -WHERE this.id = $param0 -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_director0_node:Director) - WHERE this_connect_director0_node.id = $this_connect_director0_node_param0 - CALL { - WITH * - WITH collect(this_connect_director0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_director0_node - MERGE (this)<-[:DIRECTED]-(this_connect_director0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_director_Director -} -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) -WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 -CALL { - WITH this_disconnect_director0, this_disconnect_director0_rel - WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel - UNWIND this_disconnect_director0 as x - DELETE this_disconnect_director0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_director_Director -} -WITH * -CALL { - WITH this - MATCH (this_director:\`Director\`)-[update_this0:DIRECTED]->(this) - WITH this_director { .id } AS this_director - RETURN head(collect(this_director)) AS this_director -} -WITH * -CALL { - WITH this - MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) - WITH count(this_director_Director_unique) as c - CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) - RETURN c AS this_director_Director_unique_ignored -} -RETURN collect(DISTINCT this { .id, director: this_director }) AS data" -`); + "MATCH (this:\`Movie\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_director0_node:Director) + WHERE this_connect_director0_node.id = $this_connect_director0_node_param0 + CALL { + WITH * + WITH collect(this_connect_director0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_director0_node + MERGE (this)<-[:DIRECTED]-(this_connect_director0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_director_Director + } + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) + WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 + CALL { + WITH this_disconnect_director0, this_disconnect_director0_rel + WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel + UNWIND this_disconnect_director0 as x + DELETE this_disconnect_director0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_director_Director + } + WITH * + CALL { + WITH this + MATCH (this_director:\`Director\`)-[update_this0:DIRECTED]->(this) + WITH this_director { .id } AS this_director + RETURN head(collect(this_director)) AS this_director + } + WITH * + CALL { + WITH this + MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) + WITH count(this_director_Director_unique) as c + CALL apoc.util.validate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director required', [0]) + RETURN c AS this_director_Director_unique_ignored + } + RETURN collect(DISTINCT this { .id, director: this_director }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -1415,54 +1469,54 @@ RETURN collect(DISTINCT this { .id, director: this_director }) AS data" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` -"MATCH (this:\`Movie\`) -WHERE this.id = $param0 -WITH this -CALL { - WITH this - OPTIONAL MATCH (this_connect_director0_node:Director) - WHERE this_connect_director0_node.id = $this_connect_director0_node_param0 - CALL { - WITH * - WITH collect(this_connect_director0_node) as connectedNodes, collect(this) as parentNodes - UNWIND parentNodes as this - UNWIND connectedNodes as this_connect_director0_node - MERGE (this)<-[:DIRECTED]-(this_connect_director0_node) - RETURN count(*) AS _ - } - RETURN count(*) AS connect_this_connect_director_Director -} -WITH this -CALL { -WITH this -OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) -WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 -CALL { - WITH this_disconnect_director0, this_disconnect_director0_rel - WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel - UNWIND this_disconnect_director0 as x - DELETE this_disconnect_director0_rel - RETURN count(*) AS _ -} -RETURN count(*) AS disconnect_this_disconnect_director_Director -} -WITH * -CALL { - WITH this - MATCH (this_director:\`Director\`)-[update_this0:DIRECTED]->(this) - WITH this_director { .id } AS this_director - RETURN head(collect(this_director)) AS this_director -} -WITH * -CALL { - WITH this - MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) - WITH count(this_director_Director_unique) as c - CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) - RETURN c AS this_director_Director_unique_ignored -} -RETURN collect(DISTINCT this { .id, director: this_director }) AS data" -`); + "MATCH (this:\`Movie\`) + WHERE this.id = $param0 + WITH this + CALL { + WITH this + OPTIONAL MATCH (this_connect_director0_node:Director) + WHERE this_connect_director0_node.id = $this_connect_director0_node_param0 + CALL { + WITH * + WITH collect(this_connect_director0_node) as connectedNodes, collect(this) as parentNodes + UNWIND parentNodes as this + UNWIND connectedNodes as this_connect_director0_node + MERGE (this)<-[:DIRECTED]-(this_connect_director0_node) + RETURN count(*) AS _ + } + RETURN count(*) AS connect_this_connect_director_Director + } + WITH this + CALL { + WITH this + OPTIONAL MATCH (this)<-[this_disconnect_director0_rel:DIRECTED]-(this_disconnect_director0:Director) + WHERE this_disconnect_director0.id = $updateMovies_args_disconnect_director_where_Directorparam0 + CALL { + WITH this_disconnect_director0, this_disconnect_director0_rel + WITH collect(this_disconnect_director0) as this_disconnect_director0, this_disconnect_director0_rel + UNWIND this_disconnect_director0 as x + DELETE this_disconnect_director0_rel + RETURN count(*) AS _ + } + RETURN count(*) AS disconnect_this_disconnect_director_Director + } + WITH * + CALL { + WITH this + MATCH (this_director:\`Director\`)-[update_this0:DIRECTED]->(this) + WITH this_director { .id } AS this_director + RETURN head(collect(this_director)) AS this_director + } + WITH * + CALL { + WITH this + MATCH (this)<-[this_director_Director_unique:DIRECTED]-(:Director) + WITH count(this_director_Director_unique) as c + CALL apoc.util.validate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDMovie.director must be less than or equal to one', [0]) + RETURN c AS this_director_Director_unique_ignored + } + RETURN collect(DISTINCT this { .id, director: this_director }) AS data" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/types/date.test.ts b/packages/graphql/tests/tck/types/date.test.ts index 841472094b..6eed5b1a8c 100644 --- a/packages/graphql/tests/tck/types/date.test.ts +++ b/packages/graphql/tests/tck/types/date.test.ts @@ -120,22 +120,28 @@ describe("Cypher Date", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.date = $this0_date - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.date = create_var1.date + RETURN create_this0 } - RETURN [ - this0 { .date }] AS data" + RETURN collect(create_this0 { .date }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_date\\": { - \\"year\\": 1970, - \\"month\\": 1, - \\"day\\": 1 - }, + \\"create_param0\\": [ + { + \\"date\\": { + \\"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 1a12383624..7decf8d9cf 100644 --- a/packages/graphql/tests/tck/types/datetime.test.ts +++ b/packages/graphql/tests/tck/types/datetime.test.ts @@ -94,27 +94,33 @@ describe("Cypher DateTime", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.datetime = $this0_datetime - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.datetime = create_var1.datetime + RETURN create_this0 } - RETURN [ - this0 { datetime: apoc.date.convertFormat(toString(this0.datetime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") }] AS data" + RETURN collect(create_this0 { datetime: apoc.date.convertFormat(toString(create_this0.datetime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\") }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_datetime\\": { - \\"year\\": 1970, - \\"month\\": 1, - \\"day\\": 1, - \\"hour\\": 0, - \\"minute\\": 0, - \\"second\\": 0, - \\"nanosecond\\": 0, - \\"timeZoneOffsetSeconds\\": 0 - }, + \\"create_param0\\": [ + { + \\"datetime\\": { + \\"year\\": 1970, + \\"month\\": 1, + \\"day\\": 1, + \\"hour\\": 0, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0, + \\"timeZoneOffsetSeconds\\": 0 + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/types/duration.test.ts b/packages/graphql/tests/tck/types/duration.test.ts index 8dc6598348..36d9561f95 100644 --- a/packages/graphql/tests/tck/types/duration.test.ts +++ b/packages/graphql/tests/tck/types/duration.test.ts @@ -134,29 +134,35 @@ describe("Cypher Duration", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.duration = $this0_duration - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.duration = create_var1.duration + RETURN create_this0 } - RETURN [ - this0 { .duration }] AS data" + RETURN collect(create_this0 { .duration }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_duration\\": { - \\"months\\": 24, - \\"days\\": 0, - \\"seconds\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"nanoseconds\\": { - \\"low\\": 0, - \\"high\\": 0 + \\"create_param0\\": [ + { + \\"duration\\": { + \\"months\\": 24, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } } - }, + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/types/localdatetime.test.ts b/packages/graphql/tests/tck/types/localdatetime.test.ts index ef764851d4..73b8b73a8e 100644 --- a/packages/graphql/tests/tck/types/localdatetime.test.ts +++ b/packages/graphql/tests/tck/types/localdatetime.test.ts @@ -128,26 +128,32 @@ describe("Cypher LocalDateTime", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.localDT = $this0_localDT - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.localDT = create_var1.localDT + RETURN create_this0 } - RETURN [ - this0 { .localDT }] AS data" + RETURN collect(create_this0 { .localDT }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_localDT\\": { - \\"year\\": 1974, - \\"month\\": 5, - \\"day\\": 1, - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000 - }, + \\"create_param0\\": [ + { + \\"localDT\\": { + \\"year\\": 1974, + \\"month\\": 5, + \\"day\\": 1, + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000 + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/types/localtime.test.ts b/packages/graphql/tests/tck/types/localtime.test.ts index 7baaa5279a..decdd257fd 100644 --- a/packages/graphql/tests/tck/types/localtime.test.ts +++ b/packages/graphql/tests/tck/types/localtime.test.ts @@ -122,23 +122,29 @@ describe("Cypher LocalTime", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.time = $this0_time - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.time = create_var1.time + RETURN create_this0 } - RETURN [ - this0 { .time }] AS data" + RETURN collect(create_this0 { .time }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_time\\": { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000 - }, + \\"create_param0\\": [ + { + \\"time\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000 + } + } + ], \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/types/point.test.ts b/packages/graphql/tests/tck/types/point.test.ts index 5abafa775e..080e09161b 100644 --- a/packages/graphql/tests/tck/types/point.test.ts +++ b/packages/graphql/tests/tck/types/point.test.ts @@ -629,24 +629,30 @@ describe("Cypher Points", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:PointContainer) - SET this0.point = point($this0_point) - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`PointContainer\`) + SET + create_this0.point = point(create_var1.point) + RETURN create_this0 } - RETURN [ - this0 { point: (CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } + RETURN collect(create_this0 { point: (CASE + WHEN create_this0.point IS NOT NULL THEN { point: create_this0.point, crs: create_this0.point.crs } ELSE NULL - END) }] AS data" + END) }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_point\\": { - \\"longitude\\": 1, - \\"latitude\\": 2 - }, + \\"create_param0\\": [ + { + \\"point\\": { + \\"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 cbd338757f..3d44702b1c 100644 --- a/packages/graphql/tests/tck/types/points.test.ts +++ b/packages/graphql/tests/tck/types/points.test.ts @@ -213,24 +213,30 @@ describe("Cypher Points", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:PointContainer) - SET this0.points = [p in $this0_points | point(p)] - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`PointContainer\`) + SET + create_this0.points = [create_var2 IN create_var1.points | point(create_var2)] + RETURN create_this0 } - RETURN [ - this0 { points: (CASE - WHEN this0.points IS NOT NULL THEN [p_var0 IN this0.points | { point: p_var0, crs: p_var0.crs }] + RETURN collect(create_this0 { points: (CASE + WHEN create_this0.points IS NOT NULL THEN [p_var0 IN create_this0.points | { point: p_var0, crs: p_var0.crs }] ELSE NULL - END) }] AS data" + END) }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_points\\": [ + \\"create_param0\\": [ { - \\"longitude\\": 1, - \\"latitude\\": 2 + \\"points\\": [ + { + \\"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 652c8f2c75..f98380ac59 100644 --- a/packages/graphql/tests/tck/types/time.test.ts +++ b/packages/graphql/tests/tck/types/time.test.ts @@ -124,24 +124,30 @@ describe("Cypher Time", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.time = $this0_time - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.time = create_var1.time + RETURN create_this0 } - RETURN [ - this0 { .time }] AS data" + RETURN collect(create_this0 { .time }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_time\\": { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000, - \\"timeZoneOffsetSeconds\\": -3600 - }, + \\"create_param0\\": [ + { + \\"time\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": -3600 + } + } + ], \\"resolvedCallbacks\\": {} }" `); @@ -201,24 +207,30 @@ describe("Cypher Time", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - CREATE (this0:Movie) - SET this0.time = $this0_time - RETURN this0 + "UNWIND $create_param0 AS create_var1 + CALL { + WITH create_var1 + CREATE (create_this0:\`Movie\`) + SET + create_this0.time = create_var1.time + RETURN create_this0 } - RETURN [ - this0 { .time }] AS data" + RETURN collect(create_this0 { .time }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"this0_time\\": { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 0, - \\"nanosecond\\": 0, - \\"timeZoneOffsetSeconds\\": 0 - }, + \\"create_param0\\": [ + { + \\"time\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0, + \\"timeZoneOffsetSeconds\\": 0 + } + } + ], \\"resolvedCallbacks\\": {} }" `);